docs: cleanup openApi documentation

This commit is contained in:
Gauthier Roebroeck 2025-02-17 13:06:48 +08:00
parent d4d3f641a2
commit ad8ee86a17
24 changed files with 489 additions and 33 deletions

View file

@ -6,21 +6,86 @@ import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.info.License
import io.swagger.v3.oas.models.security.SecurityScheme
import io.swagger.v3.oas.models.tags.Tag
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.ANNOUNCEMENTS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.API_KEYS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOKS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOK_FONTS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOK_IMPORT
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOK_PAGES
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOK_POSTER
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOK_WEBPUB
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.CLAIM
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.CLIENT_SETTINGS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.COLLECTIONS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.COLLECTION_POSTER
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.COLLECTION_SERIES
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.COMICRACK
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.CURRENT_USER
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.DEPRECATED
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.DUPLICATE_PAGES
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.FILE_SYSTEM
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.HISTORY
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.LIBRARIES
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.LOGIN
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.MIHON
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.OAUTH2
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.READLISTS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.READLIST_BOOKS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.READLIST_POSTER
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.REFERENTIAL
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.RELEASES
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.SERIES
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.SERIES_POSTER
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.SERVER_SETTINGS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.SYNCPOINTS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.TASKS
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.USERS
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class OpenApiConfiguration {
class OpenApiConfiguration(
@Value("\${application.version}") private val appVersion: String,
) {
@Bean
fun openApi(): OpenAPI =
OpenAPI()
.info(
Info()
.title("Komga API")
.version("v1.0")
.version(appVersion)
.description(
"""
Komga RESTful API.
# Authentication
Most endpoints require authentication. Authentication is done using either:
- Basic Authentication
- Passing an API Key in the `X-API-Key` header
# Sessions
Upon successful authentication, a session is created, and can be reused.
- By default, a `SESSION` cookie is set via `Set-Cookie` response header. This works well for browsers and clients that can handle cookies.
- If you specify a header `X-Auth-Token` during authentication, the session ID will be returned via this same header. You can then pass that header again for subsequent requests to reuse the session.
If you need to set the session cookie later on, you can call `/api/v1/login/set-cookie` with `X-Auth-Token`. The response will contain the `Set-Cookie` header.
# Remember Me
During authentication, if a request parameter `remember-me` is passed and set to `true`, the server will also return a `remember-me` cookie. This cookie will be used to login automatically even if the session has expired.
# Logout
You can explicitly logout an existing session by calling `/api/logout`. This would return a `204`.
# Deprecation
API endpoints marked as deprecated will be removed in the next major version.
""".trimIndent(),
).license(License().name("MIT").url("https://github.com/gotson/komga/blob/master/LICENSE")),
).externalDocs(
@ -41,5 +106,183 @@ class OpenApiConfiguration {
.`in`(SecurityScheme.In.HEADER)
.name("X-API-Key"),
),
)
).tags(tags)
.extensions(mapOf("x-tagGroups" to tagGroups))
data class TagGroup(
val name: String,
val tags: List<String>,
)
private val tagGroups =
listOf(
TagGroup(
"Libraries",
listOf(
LIBRARIES,
),
),
TagGroup(
"Series",
listOf(
SERIES,
SERIES_POSTER,
),
),
TagGroup(
"Books",
listOf(
BOOKS,
BOOK_PAGES,
BOOK_POSTER,
BOOK_IMPORT,
DUPLICATE_PAGES,
BOOK_WEBPUB,
BOOK_FONTS,
),
),
TagGroup(
"Collections",
listOf(
COLLECTIONS,
COLLECTION_SERIES,
COLLECTION_POSTER,
),
),
TagGroup(
"Readlists",
listOf(
READLISTS,
READLIST_BOOKS,
READLIST_POSTER,
),
),
TagGroup(
"Referential",
listOf(
REFERENTIAL,
),
),
TagGroup(
"Users",
listOf(
CURRENT_USER,
USERS,
API_KEYS,
LOGIN,
OAUTH2,
SYNCPOINTS,
),
),
TagGroup(
"Server",
listOf(
CLAIM,
SERVER_SETTINGS,
TASKS,
HISTORY,
FILE_SYSTEM,
RELEASES,
ANNOUNCEMENTS,
),
),
TagGroup(
"Integrations",
listOf(
CLIENT_SETTINGS,
MIHON,
COMICRACK,
),
),
TagGroup(
"Deprecation",
listOf(
DEPRECATED,
),
),
)
object TagNames {
const val LIBRARIES = "Libraries"
const val SERIES = "Series"
const val SERIES_POSTER = "Series Poster"
const val BOOKS = "Books"
const val BOOK_POSTER = "Book Poster"
const val BOOK_PAGES = "Book Pages"
const val BOOK_WEBPUB = "WebPub Manifest"
const val BOOK_IMPORT = "Import"
const val BOOK_FONTS = "Fonts"
const val DUPLICATE_PAGES = "Duplicate Pages"
const val COLLECTIONS = "Collections"
const val COLLECTION_SERIES = "Collection Series"
const val COLLECTION_POSTER = "Collection Poster"
const val READLISTS = "Readlists"
const val READLIST_BOOKS = "Readlist Books"
const val READLIST_POSTER = "Readlist Poster"
const val REFERENTIAL = "Referential metadata"
const val CURRENT_USER = "Current user"
const val USERS = "Users"
const val API_KEYS = "API Keys"
const val LOGIN = "User login"
const val OAUTH2 = "OAuth2"
const val SYNCPOINTS = "Sync points"
const val CLAIM = "Claim server"
const val TASKS = "Tasks"
const val HISTORY = "History"
const val FILE_SYSTEM = "File system"
const val SERVER_SETTINGS = "Server settings"
const val RELEASES = "Releases"
const val ANNOUNCEMENTS = "Announcements"
const val MIHON = "Mihon"
const val COMICRACK = "ComicRack"
const val CLIENT_SETTINGS = "Client settings"
const val DEPRECATED = "Deprecated"
}
private val tags =
listOf(
Tag().name(LIBRARIES).description("Manage libraries."),
Tag().name(SERIES).description("Manage series."),
Tag().name(SERIES_POSTER).description("Manage posters for series."),
Tag().name(BOOKS).description("Manage books."),
Tag().name(BOOK_POSTER).description("Manage posters for books."),
Tag().name(BOOK_PAGES),
Tag().name(BOOK_WEBPUB),
Tag().name(BOOK_IMPORT),
Tag().name(BOOK_FONTS).description("Provide font files and CSS for the Epub Reader."),
Tag().name(DUPLICATE_PAGES).description("Manage duplicate pages. Duplicate pages are identified by a page hash."),
Tag().name(COLLECTIONS).description("Manage collections."),
Tag().name(COLLECTION_POSTER).description("Manage posters for collections."),
Tag().name(COLLECTION_SERIES),
Tag().name(READLISTS).description("Manage readlists."),
Tag().name(READLIST_POSTER).description("Manage posters for readlists."),
Tag().name(READLIST_BOOKS),
Tag().name(REFERENTIAL).description("Retrieve referential metadata from all items in the Komga server."),
Tag().name(CURRENT_USER).description("Manage current user."),
Tag().name(USERS).description("Manage users."),
Tag().name(API_KEYS).description("Manage API Keys"),
Tag().name(LOGIN),
Tag().name(OAUTH2).description("List registered OAuth2 providers"),
Tag().name(SYNCPOINTS).description("Sync points are automatically created during a Kobo sync."),
Tag().name(CLAIM).description("Claim a freshly installed Komga server."),
Tag().name(TASKS).description("Manage server tasks"),
Tag().name(HISTORY).description("Server events history"),
Tag().name(FILE_SYSTEM).description("List files from the host server's file system"),
Tag().name(SERVER_SETTINGS).description("Store and retrieve server settings"),
Tag().name(RELEASES).description("Retrieve releases information"),
Tag().name(ANNOUNCEMENTS).description("Retrieve announcements from the Komga website"),
Tag().name(MIHON),
Tag().name(COMICRACK),
Tag().name(CLIENT_SETTINGS).description("Store and retrieve global and per-user settings. Those settings are not used by Komga itself, but can be stored for convenience by client applications."),
Tag().name(DEPRECATED),
)
}

View file

@ -24,6 +24,7 @@ import org.gotson.komga.domain.service.BookLifecycle
import org.gotson.komga.infrastructure.image.ImageType
import org.gotson.komga.infrastructure.mediacontainer.ContentDetector
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault
import org.gotson.komga.interfaces.api.dto.MEDIATYPE_PROGRESSION_JSON_VALUE
import org.gotson.komga.interfaces.api.persistence.BookDtoRepository
@ -190,6 +191,7 @@ class CommonBookController(
}
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get raw book page", description = "Returns the book page in raw format, without content negotiation.", tags = [OpenApiConfiguration.TagNames.BOOK_PAGES])
@GetMapping(
value = [
"api/v1/books/{bookId}/pages/{pageNumber}/raw",
@ -252,6 +254,7 @@ class CommonBookController(
throw ResponseStatusException(HttpStatus.NOT_FOUND, "File not found, it may have moved")
}
@Operation(summary = "Get Eoub resource", description = "Return a resource from within an Epub book.", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB])
@GetMapping(
value = [
"api/v1/books/{bookId}/resource/{*resource}",
@ -306,7 +309,7 @@ class CommonBookController(
.body(bytes)
}
@Operation(description = "Download the book file.")
@Operation(summary = "Download book file", description = "Download the book file.", tags = [OpenApiConfiguration.TagNames.BOOKS])
@GetMapping(
value = [
"api/v1/books/{bookId}/file",
@ -360,6 +363,7 @@ class CommonBookController(
}
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get book progression", description = "The Progression API is a proposed standard for OPDS 2 and Readium. It is used by the Epub Reader.", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB])
@GetMapping(
value = [
"api/v1/books/{bookId}/progression",
@ -379,6 +383,7 @@ class CommonBookController(
} ?: ResponseEntity.noContent().build()
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Mark book progression", description = "The Progression API is a proposed standard for OPDS 2 and Readium. It is used by the Epub Reader.", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB])
@PutMapping(
value = [
"api/v1/books/{bookId}/progression",

View file

@ -1,8 +1,11 @@
package org.gotson.komga.interfaces.api.rest
import com.github.benmanes.caffeine.cache.Caffeine
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.gotson.komga.domain.persistence.KomgaUserRepository
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.interfaces.api.rest.dto.JsonFeedDto
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
@ -22,6 +25,7 @@ private const val WEBSITE = "https://komga.org"
@RestController
@RequestMapping("api/v1/announcements", produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = OpenApiConfiguration.TagNames.ANNOUNCEMENTS)
class AnnouncementController(
private val userRepository: KomgaUserRepository,
webClientBuilder: WebClient.Builder,
@ -36,6 +40,7 @@ class AnnouncementController(
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Retrieve announcements")
fun getAnnouncements(
@AuthenticationPrincipal principal: KomgaPrincipal,
): JsonFeedDto =
@ -50,6 +55,7 @@ class AnnouncementController(
@PreAuthorize("hasRole('ADMIN')")
@PutMapping
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Mark announcements as read")
fun markAnnouncementsRead(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestBody announcementIds: Set<String>,

View file

@ -37,6 +37,7 @@ import org.gotson.komga.infrastructure.image.ImageAnalyzer
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
import org.gotson.komga.infrastructure.mediacontainer.ContentDetector
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault
@ -118,7 +119,7 @@ class BookController(
@Deprecated("use /v1/books/list instead")
@PageableAsQueryParam
@GetMapping("api/v1/books")
@Operation(summary = "Use POST /api/v1/books/list instead")
@Operation(summary = "List books", description = "Use POST /api/v1/books/list instead. Deprecated since 1.19.0.", tags = [OpenApiConfiguration.TagNames.BOOKS, OpenApiConfiguration.TagNames.DEPRECATED])
fun getAllBooks(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "search", required = false) searchTerm: String? = null,
@ -170,6 +171,7 @@ class BookController(
@PageableAsQueryParam
@PostMapping("api/v1/books/list")
@Operation(summary = "List books", tags = [OpenApiConfiguration.TagNames.BOOKS])
fun getBooksList(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestBody search: BookSearch,
@ -198,7 +200,7 @@ class BookController(
.map { it.restrictUrl(!principal.user.isAdmin) }
}
@Operation(description = "Return newly added or updated books.")
@Operation(summary = "List latest books", description = "Return newly added or updated books.", tags = [OpenApiConfiguration.TagNames.BOOKS])
@PageableWithoutSortAsQueryParam
@GetMapping("api/v1/books/latest")
fun getLatestBooks(
@ -225,7 +227,7 @@ class BookController(
).map { it.restrictUrl(!principal.user.isAdmin) }
}
@Operation(description = "Return first unread book of series with at least one book read and no books in progress.")
@Operation(summary = "List books on deck", description = "Return first unread book of series with at least one book read and no books in progress.", tags = [OpenApiConfiguration.TagNames.BOOKS])
@PageableWithoutSortAsQueryParam
@GetMapping("api/v1/books/ondeck")
fun getBooksOnDeck(
@ -241,6 +243,7 @@ class BookController(
principal.user.restrictions,
).map { it.restrictUrl(!principal.user.isAdmin) }
@Operation(summary = "List duplicate books", description = "Return books that have the same file hash.", tags = [OpenApiConfiguration.TagNames.BOOKS])
@PageableAsQueryParam
@GetMapping("api/v1/books/duplicates")
@PreAuthorize("hasRole('ADMIN')")
@ -268,6 +271,7 @@ class BookController(
return bookDtoRepository.findAllDuplicates(principal.user.id, pageRequest)
}
@Operation(summary = "Get book details", tags = [OpenApiConfiguration.TagNames.BOOKS])
@GetMapping("api/v1/books/{bookId}")
fun getOneBook(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -279,6 +283,7 @@ class BookController(
it.restrictUrl(!principal.user.isAdmin)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get previous book in series", tags = [OpenApiConfiguration.TagNames.BOOKS])
@GetMapping("api/v1/books/{bookId}/previous")
fun getBookSiblingPrevious(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -292,6 +297,7 @@ class BookController(
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Get next book in series", tags = [OpenApiConfiguration.TagNames.BOOKS])
@GetMapping("api/v1/books/{bookId}/next")
fun getBookSiblingNext(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -305,6 +311,7 @@ class BookController(
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "List book's readlists", tags = [OpenApiConfiguration.TagNames.BOOKS])
@GetMapping("api/v1/books/{bookId}/readlists")
fun getAllReadListsByBook(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -317,6 +324,7 @@ class BookController(
.map { it.toDto() }
}
@Operation(summary = "Get book's poster image", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
@GetMapping(
value = ["api/v1/books/{bookId}/thumbnail"],
@ -331,6 +339,7 @@ class BookController(
return bookLifecycle.getThumbnailBytes(bookId)?.bytes ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Get book poster image", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
@GetMapping(value = ["api/v1/books/{bookId}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE])
fun getBookThumbnailById(
@ -344,6 +353,7 @@ class BookController(
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "List book posters", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER])
@GetMapping(value = ["api/v1/books/{bookId}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE])
fun getBookThumbnails(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -356,6 +366,7 @@ class BookController(
.map { it.toDto() }
}
@Operation(summary = "Add book poster", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER])
@PostMapping(value = ["api/v1/books/{bookId}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@PreAuthorize("hasRole('ADMIN')")
fun addUserUploadedBookThumbnail(
@ -385,6 +396,7 @@ class BookController(
).toDto()
}
@Operation(summary = "Mark book poster as selected", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER])
@PutMapping("api/v1/books/{bookId}/thumbnails/{thumbnailId}/selected")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -399,6 +411,7 @@ class BookController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Delete book poster", description = "Only uploaded posters can be deleted.", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER])
@DeleteMapping("api/v1/books/{bookId}/thumbnails/{thumbnailId}")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -416,6 +429,7 @@ class BookController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "List book pages", tags = [OpenApiConfiguration.TagNames.BOOK_PAGES])
@GetMapping("api/v1/books/{bookId}/pages")
fun getBookPages(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -450,6 +464,7 @@ class BookController(
}
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get book page image", tags = [OpenApiConfiguration.TagNames.BOOK_PAGES])
@ApiResponse(content = [Content(mediaType = "image/*", schema = Schema(type = "string", format = "binary"))])
@GetMapping(
value = ["api/v1/books/{bookId}/pages/{pageNumber}"],
@ -477,6 +492,7 @@ class BookController(
contentNegotiation: Boolean,
): ResponseEntity<ByteArray> = commonBookController.getBookPageInternal(bookId, if (zeroBasedIndex) pageNumber + 1 else pageNumber, convertTo, request, principal, if (contentNegotiation) acceptHeaders else null)
@Operation(summary = "Get book page thumbnail", description = "The image is resized to 300px on the largest dimension.", tags = [OpenApiConfiguration.TagNames.BOOK_PAGES])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
@GetMapping(
value = ["api/v1/books/{bookId}/pages/{pageNumber}/thumbnail"],
@ -519,6 +535,7 @@ class BookController(
}
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get book's WebPub manifest", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB])
@GetMapping(
value = ["api/v1/books/{bookId}/manifest"],
produces = [MEDIATYPE_WEBPUB_JSON_VALUE, MEDIATYPE_DIVINA_JSON_VALUE],
@ -534,6 +551,7 @@ class BookController(
.body(manifest)
}
@Operation(summary = "List book's positions", description = "The Positions API is a proposed standard for OPDS 2 and Readium. It is used by the Epub Reader.", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB])
@GetMapping(
value = ["api/v1/books/{bookId}/positions"],
produces = [MEDIATYPE_POSITION_LIST_JSON_VALUE],
@ -566,6 +584,7 @@ class BookController(
.body(R2Positions(extension.positions.size, extension.positions))
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get book's WebPub manifest (Epub)", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB])
@GetMapping(
value = ["api/v1/books/{bookId}/manifest/epub"],
produces = [MEDIATYPE_WEBPUB_JSON_VALUE],
@ -575,6 +594,7 @@ class BookController(
@PathVariable bookId: String,
): WPPublicationDto = commonBookController.getWebPubManifestEpubInternal(principal, bookId, webPubGenerator)
@Operation(summary = "Get book's WebPub manifest (PDF)", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB])
@GetMapping(
value = ["api/v1/books/{bookId}/manifest/pdf"],
produces = [MEDIATYPE_WEBPUB_JSON_VALUE],
@ -584,6 +604,7 @@ class BookController(
@PathVariable bookId: String,
): WPPublicationDto = commonBookController.getWebPubManifestPdfInternal(principal, bookId, webPubGenerator)
@Operation(summary = "Get book's WebPub manifest (DiViNa)", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB])
@GetMapping(
value = ["api/v1/books/{bookId}/manifest/divina"],
produces = [MEDIATYPE_DIVINA_JSON_VALUE],
@ -593,6 +614,7 @@ class BookController(
@PathVariable bookId: String,
): WPPublicationDto = commonBookController.getWebPubManifestDivinaInternal(principal, bookId, webPubGenerator)
@Operation(summary = "Analyze book", tags = [OpenApiConfiguration.TagNames.BOOKS])
@PostMapping("api/v1/books/{bookId}/analyze")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -604,6 +626,7 @@ class BookController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Refresh book metadata", tags = [OpenApiConfiguration.TagNames.BOOKS])
@PostMapping("api/v1/books/{bookId}/metadata/refresh")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -616,6 +639,7 @@ class BookController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Update book metadata", description = "Set a field to null to unset the metadata. You can omit fields you don't want to update.", tags = [OpenApiConfiguration.TagNames.BOOKS])
@PatchMapping("api/v1/books/{bookId}/metadata")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ -635,6 +659,7 @@ class BookController(
}
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Update book metadata in bulk", description = "Set a field to null to unset the metadata. You can omit fields you don't want to update.", tags = [OpenApiConfiguration.TagNames.BOOKS])
@PatchMapping("api/v1/books/metadata")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ -658,7 +683,7 @@ class BookController(
updatedBooks.map { it.seriesId }.distinct().forEach { taskEmitter.aggregateSeriesMetadata(it) }
}
@Operation(description = "Mark book as read and/or change page progress")
@Operation(summary = "Mark book's read progress", description = "Mark book as read and/or change page progress.", tags = [OpenApiConfiguration.TagNames.BOOKS])
@PatchMapping("api/v1/books/{bookId}/read-progress")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun markReadProgress(
@ -683,7 +708,7 @@ class BookController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(description = "Mark book as unread")
@Operation(summary = "Mark book as unread", description = "Mark book as unread", tags = [OpenApiConfiguration.TagNames.BOOKS])
@DeleteMapping("api/v1/books/{bookId}/read-progress")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteReadProgress(
@ -697,6 +722,7 @@ class BookController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Import books", tags = [OpenApiConfiguration.TagNames.BOOK_IMPORT])
@PostMapping("api/v1/books/import")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -719,10 +745,11 @@ class BookController(
}
}
@Operation(summary = "Delete book file", tags = [OpenApiConfiguration.TagNames.BOOKS])
@DeleteMapping("api/v1/books/{bookId}/file")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
fun deleteBook(
fun deleteBookFile(
@PathVariable bookId: String,
) {
taskEmitter.deleteBook(
@ -731,6 +758,7 @@ class BookController(
)
}
@Operation(summary = "Regenerate books posters", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER])
@PutMapping("api/v1/books/thumbnails")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)

View file

@ -1,10 +1,13 @@
package org.gotson.komga.interfaces.api.rest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank
import org.gotson.komga.domain.model.KomgaUser
import org.gotson.komga.domain.model.UserRoles
import org.gotson.komga.domain.service.KomgaUserLifecycle
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.interfaces.api.rest.dto.UserDto
import org.gotson.komga.interfaces.api.rest.dto.toDto
import org.springframework.http.HttpStatus
@ -19,14 +22,17 @@ import org.springframework.web.server.ResponseStatusException
@RestController
@RequestMapping("api/v1/claim", produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = OpenApiConfiguration.TagNames.CLAIM)
@Validated
class ClaimController(
private val userDetailsLifecycle: KomgaUserLifecycle,
) {
@GetMapping
@Operation(summary = "Retrieve claim status", description = "Check whether this server has already been claimed.")
fun getClaimStatus() = ClaimStatus(userDetailsLifecycle.countUsers() > 0)
@PostMapping
@Operation(summary = "Claim server", description = "Creates an admin user with the provided credentials.")
fun claimAdmin(
@Email(regexp = ".+@.+\\..+")
@RequestHeader("X-Komga-Email")

View file

@ -9,6 +9,7 @@ import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Pattern
import org.gotson.komga.infrastructure.jooq.main.ClientSettingsDtoDao
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.interfaces.api.rest.dto.ClientSettingDto
import org.gotson.komga.interfaces.api.rest.dto.ClientSettingGlobalUpdateDto
import org.gotson.komga.interfaces.api.rest.dto.ClientSettingUserUpdateDto
@ -30,13 +31,7 @@ private const val KEY_REGEX = """^[a-z](?:[a-z0-9_-]*[a-z0-9])*(?:\.[a-z0-9](?:[
@RestController
@RequestMapping(value = ["api/v1/client-settings"], produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(
name = "Client Settings",
description = """
Store and retrieve global and per-user settings.
Those settings are not used by Komga itself, but can be stored for convenience by client applications.
""",
)
@Tag(name = OpenApiConfiguration.TagNames.CLIENT_SETTINGS)
@Validated
class ClientSettingsController(
private val clientSettingsDtoDao: ClientSettingsDtoDao,
@ -48,7 +43,7 @@ class ClientSettingsController(
): Map<String, ClientSettingDto> = clientSettingsDtoDao.findAllGlobal(principal == null)
@GetMapping("user/list")
@Operation(summary = "Retrieve client settings for the current user")
@Operation(summary = "Retrieve user client settings")
fun getUserSettings(
@AuthenticationPrincipal principal: KomgaPrincipal,
): Map<String, ClientSettingDto> = clientSettingsDtoDao.findAllUser(principal.user.id)
@ -95,7 +90,7 @@ class ClientSettingsController(
@PatchMapping("user")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Save user settings for the current user", description = "Setting key should be a valid lowercase namespace string like 'application.domain.key'")
@Operation(summary = "Save user settings", description = "Setting key should be a valid lowercase namespace string like 'application.domain.key'")
@OASRequestBody(
content = [
Content(
@ -155,7 +150,7 @@ class ClientSettingsController(
@DeleteMapping("user")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Delete user settings for the current user", description = "Setting key should be a valid lowercase namespace string like 'application.domain.key'")
@Operation(summary = "Delete user settings", description = "Setting key should be a valid lowercase namespace string like 'application.domain.key'")
@OASRequestBody(
content = [
Content(

View file

@ -1,6 +1,9 @@
package org.gotson.komga.interfaces.api.rest
import com.fasterxml.jackson.annotation.JsonInclude
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.access.prepost.PreAuthorize
@ -18,10 +21,15 @@ import kotlin.streams.asSequence
@RestController
@RequestMapping("api/v1/filesystem", produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = OpenApiConfiguration.TagNames.FILE_SYSTEM)
class FileSystemController {
private val fs = FileSystems.getDefault()
@PostMapping
@Operation(
summary = "Directory listing",
description = "List folders and files from the host server's file system. If no request body is passed then the root directories are returned.",
)
fun getDirectoryListing(
@RequestBody(required = false) request: DirectoryRequestDto = DirectoryRequestDto(),
): DirectoryListingDto =

View file

@ -1,7 +1,10 @@
package org.gotson.komga.interfaces.api.rest
import io.github.oshai.kotlinlogging.KotlinLogging
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.gotson.komga.infrastructure.configuration.KomgaProperties
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.language.contains
import org.springframework.core.io.ByteArrayResource
import org.springframework.core.io.FileSystemResource
@ -28,6 +31,7 @@ private val logger = KotlinLogging.logger {}
@RestController
@RequestMapping(value = ["api/v1/fonts"], produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = OpenApiConfiguration.TagNames.BOOK_FONTS)
class FontsController(
komgaProperties: KomgaProperties,
) {
@ -81,9 +85,11 @@ class FontsController(
}
@GetMapping("families")
@Operation(summary = "List font families", description = "List all available font families.")
fun listFonts(): Set<String> = fonts.keys
@GetMapping("resource/{fontFamily}/{fontFile}")
@Operation(summary = "Download font file")
fun getFontFile(
@PathVariable fontFamily: String,
@PathVariable fontFile: String,
@ -105,6 +111,7 @@ class FontsController(
}
@GetMapping("resource/{fontFamily}/css", produces = ["text/css"])
@Operation(summary = "Download CSS file", description = "Download a CSS file with the @font-face block for the font family. This is used by the Epub Reader to change fonts.")
fun getFontFamilyAsCss(
@PathVariable fontFamily: String,
): ResponseEntity<Resource> {

View file

@ -1,6 +1,9 @@
package org.gotson.komga.interfaces.api.rest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.tags.Tag
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
import org.gotson.komga.interfaces.api.persistence.HistoricalEventDtoRepository
import org.gotson.komga.interfaces.api.rest.dto.HistoricalEventDto
@ -17,11 +20,13 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("api/v1/history", produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = OpenApiConfiguration.TagNames.HISTORY)
class HistoricalEventController(
private val historicalEventDtoRepository: HistoricalEventDtoRepository,
) {
@GetMapping
@PageableAsQueryParam
@Operation(summary = "List historical events")
fun getAll(
@Parameter(hidden = true) page: Pageable,
): Page<HistoricalEventDto> {

View file

@ -2,6 +2,7 @@ package org.gotson.komga.interfaces.api.rest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.gotson.komga.application.tasks.HIGHEST_PRIORITY
import org.gotson.komga.application.tasks.HIGH_PRIORITY
@ -18,6 +19,7 @@ import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.persistence.SeriesRepository
import org.gotson.komga.domain.service.LibraryLifecycle
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.web.filePathToUrl
import org.gotson.komga.interfaces.api.rest.dto.LibraryCreationDto
import org.gotson.komga.interfaces.api.rest.dto.LibraryDto
@ -45,6 +47,7 @@ import java.io.FileNotFoundException
@RestController
@RequestMapping("api/v1/libraries", produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = OpenApiConfiguration.TagNames.LIBRARIES)
class LibraryController(
private val taskEmitter: TaskEmitter,
private val libraryLifecycle: LibraryLifecycle,
@ -53,6 +56,10 @@ class LibraryController(
private val seriesRepository: SeriesRepository,
) {
@GetMapping
@Operation(
summary = "List all libraries",
description = "The libraries are filtered based on the current user's permissions",
)
fun getAll(
@AuthenticationPrincipal principal: KomgaPrincipal,
): List<LibraryDto> =
@ -63,6 +70,7 @@ class LibraryController(
}.sortedBy { it.name.lowercase() }.map { it.toDto(includeRoot = principal.user.isAdmin) }
@GetMapping("{libraryId}")
@Operation(summary = "Get details for a single library")
fun getOne(
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable libraryId: String,
@ -74,6 +82,7 @@ class LibraryController(
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Create a library")
fun addOne(
@AuthenticationPrincipal principal: KomgaPrincipal,
@Valid @RequestBody
@ -130,7 +139,7 @@ class LibraryController(
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Deprecated("Use PATCH /v1/libraries/{libraryId} instead", ReplaceWith("patchOne"))
@Operation(summary = "Use PATCH /api/v1/libraries/{libraryId} instead")
@Operation(summary = "Update a library", description = "Use PATCH /api/v1/libraries/{libraryId} instead. Deprecated since 1.3.0.", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
fun updateOne(
@PathVariable libraryId: String,
@Valid @RequestBody
@ -142,6 +151,7 @@ class LibraryController(
@PatchMapping("/{libraryId}")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Update a library", description = "You can omit fields you don't want to update")
fun patchOne(
@PathVariable libraryId: String,
@Parameter(description = "Fields to update. You can omit fields you don't want to update.")
@ -204,6 +214,7 @@ class LibraryController(
@DeleteMapping("/{libraryId}")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Delete a library")
fun deleteOne(
@PathVariable libraryId: String,
) {
@ -215,6 +226,7 @@ class LibraryController(
@PostMapping("{libraryId}/scan")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@Operation(summary = "Scan a library")
fun scan(
@PathVariable libraryId: String,
@RequestParam(required = false) deep: Boolean = false,
@ -227,6 +239,7 @@ class LibraryController(
@PostMapping("{libraryId}/analyze")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@Operation(summary = "Analyze a library")
fun analyze(
@PathVariable libraryId: String,
) {
@ -243,6 +256,7 @@ class LibraryController(
@PostMapping("{libraryId}/metadata/refresh")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@Operation(summary = "Refresh metadata for a library")
fun refreshMetadata(
@PathVariable libraryId: String,
) {
@ -261,6 +275,7 @@ class LibraryController(
@PostMapping("{libraryId}/empty-trash")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@Operation(summary = "Empty trash for a library")
fun emptyTrash(
@PathVariable libraryId: String,
) {

View file

@ -1,8 +1,11 @@
package org.gotson.komga.interfaces.api.rest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import jakarta.servlet.http.HttpSession
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.session.web.http.CookieSerializer
@ -13,9 +16,11 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = OpenApiConfiguration.TagNames.LOGIN)
class LoginController(
private val cookieSerializer: CookieSerializer,
) {
@Operation(summary = "Set cookie", description = "Forcefully return Set-Cookie header, even if the session is contained in the X-Auth-Token header.")
@GetMapping("api/v1/login/set-cookie")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun headerToCookie(

View file

@ -1,5 +1,8 @@
package org.gotson.komga.interfaces.api.rest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.springframework.http.MediaType
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository
import org.springframework.web.bind.annotation.GetMapping
@ -8,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("api/v1/oauth2", produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = OpenApiConfiguration.TagNames.OAUTH2)
class OAuth2Controller(
clientRegistrationRepository: InMemoryClientRegistrationRepository?,
) {
@ -17,6 +21,7 @@ class OAuth2Controller(
} ?: emptyList()
@GetMapping("providers")
@Operation(summary = "List registered OAuth2 providers")
fun getProviders() = registrationIds
}

View file

@ -1,15 +1,18 @@
package org.gotson.komga.interfaces.api.rest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.gotson.komga.application.tasks.TaskEmitter
import org.gotson.komga.domain.model.BookPageNumbered
import org.gotson.komga.domain.model.PageHashKnown
import org.gotson.komga.domain.persistence.PageHashRepository
import org.gotson.komga.domain.service.PageHashLifecycle
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault
import org.gotson.komga.interfaces.api.rest.dto.PageHashCreationDto
@ -37,11 +40,13 @@ import org.springframework.web.server.ResponseStatusException
@RestController
@RequestMapping("api/v1/page-hashes", produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = OpenApiConfiguration.TagNames.DUPLICATE_PAGES)
class PageHashController(
private val pageHashRepository: PageHashRepository,
private val pageHashLifecycle: PageHashLifecycle,
private val taskEmitter: TaskEmitter,
) {
@Operation(summary = "List known duplicates")
@GetMapping
@PageableAsQueryParam
fun getKnownPageHashes(
@ -49,6 +54,7 @@ class PageHashController(
@Parameter(hidden = true) page: Pageable,
): Page<PageHashKnownDto> = pageHashRepository.findAllKnown(actions, page).map { it.toDto() }
@Operation(summary = "Get known duplicate image thumbnail")
@GetMapping("/{pageHash}/thumbnail", produces = [MediaType.IMAGE_JPEG_VALUE])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
fun getKnownPageHashThumbnail(
@ -57,12 +63,14 @@ class PageHashController(
pageHashRepository.getKnownThumbnail(pageHash)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "List unknown duplicates")
@GetMapping("/unknown")
@PageableAsQueryParam
fun getUnknownPageHashes(
@Parameter(hidden = true) page: Pageable,
): Page<PageHashUnknownDto> = pageHashRepository.findAllUnknown(page).map { it.toDto() }
@Operation(summary = "List duplicate matches")
@GetMapping("{pageHash}")
@PageableAsQueryParam
fun getPageHashMatches(
@ -75,6 +83,7 @@ class PageHashController(
page,
).map { it.toDto() }
@Operation(summary = "Get unknown duplicate image thumbnail")
@GetMapping("unknown/{pageHash}/thumbnail", produces = [MediaType.IMAGE_JPEG_VALUE])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
fun getUnknownPageHashThumbnail(
@ -88,6 +97,7 @@ class PageHashController(
.body(it.bytes)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Mark duplicate page as known")
@PutMapping
@ResponseStatus(HttpStatus.ACCEPTED)
fun createOrUpdateKnownPageHash(
@ -107,6 +117,7 @@ class PageHashController(
}
}
@Operation(summary = "Delete all duplicate pages by hash")
@PostMapping("{pageHash}/delete-all")
@ResponseStatus(HttpStatus.ACCEPTED)
fun performDelete(
@ -131,6 +142,7 @@ class PageHashController(
taskEmitter.removeDuplicatePages(toRemove)
}
@Operation(summary = "Delete specific duplicate page")
@PostMapping("{pageHash}/delete-match")
@ResponseStatus(HttpStatus.ACCEPTED)
fun deleteSingleMatch(

View file

@ -1,6 +1,7 @@
package org.gotson.komga.interfaces.api.rest
import io.github.oshai.kotlinlogging.KotlinLogging
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
@ -34,6 +35,7 @@ import org.gotson.komga.infrastructure.jooq.UnpagedSorted
import org.gotson.komga.infrastructure.mediacontainer.ContentDetector
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
import org.gotson.komga.infrastructure.web.Authors
import org.gotson.komga.interfaces.api.persistence.BookDtoRepository
@ -98,6 +100,7 @@ class ReadListController(
private val bookLifecycle: BookLifecycle,
private val eventPublisher: ApplicationEventPublisher,
) {
@Operation(summary = "List readlists", tags = [OpenApiConfiguration.TagNames.READLISTS])
@PageableWithoutSortAsQueryParam
@GetMapping
fun getAll(
@ -129,6 +132,7 @@ class ReadListController(
.map { it.toDto() }
}
@Operation(summary = "Get readlist details", tags = [OpenApiConfiguration.TagNames.READLISTS])
@GetMapping("{id}")
fun getOne(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -139,6 +143,7 @@ class ReadListController(
?.toDto()
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get readlist's poster image", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
@GetMapping(value = ["{id}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE])
fun getReadListThumbnail(
@ -153,6 +158,7 @@ class ReadListController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Get readlist poster image", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
@GetMapping(value = ["{id}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE])
fun getReadListThumbnailById(
@ -166,6 +172,7 @@ class ReadListController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "List readlist's posters", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER])
@GetMapping(value = ["{id}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE])
fun getReadListThumbnails(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -176,6 +183,7 @@ class ReadListController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Add readlist poster", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER])
@PostMapping(value = ["{id}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@PreAuthorize("hasRole('ADMIN')")
fun addUserUploadedReadListThumbnail(
@ -205,6 +213,7 @@ class ReadListController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Mark readlist poster as selected", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER])
@PutMapping("{id}/thumbnails/{thumbnailId}/selected")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -221,6 +230,7 @@ class ReadListController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Delete readlist poster", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER])
@DeleteMapping("{id}/thumbnails/{thumbnailId}")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -236,6 +246,7 @@ class ReadListController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Create readlist", tags = [OpenApiConfiguration.TagNames.READLISTS])
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
fun addOne(
@ -256,6 +267,7 @@ class ReadListController(
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message)
}
@Operation(summary = "Match ComicRack list", tags = [OpenApiConfiguration.TagNames.COMICRACK])
@PostMapping("match/comicrack")
@PreAuthorize("hasRole('ADMIN')")
fun matchFromComicRackList(
@ -267,6 +279,7 @@ class ReadListController(
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.code)
}
@Operation(summary = "Update readlist", tags = [OpenApiConfiguration.TagNames.READLISTS])
@PatchMapping("{id}")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ -291,6 +304,7 @@ class ReadListController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Delete readlist", tags = [OpenApiConfiguration.TagNames.READLISTS])
@DeleteMapping("{id}")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ -302,6 +316,7 @@ class ReadListController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "List readlist's books", tags = [OpenApiConfiguration.TagNames.READLIST_BOOKS])
@PageableWithoutSortAsQueryParam
@AuthorsAsQueryParam
@GetMapping("{id}/books")
@ -353,6 +368,7 @@ class ReadListController(
.map { it.restrictUrl(!principal.user.isAdmin) }
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get previous book in readlist", tags = [OpenApiConfiguration.TagNames.READLIST_BOOKS])
@GetMapping("{id}/books/{bookId}/previous")
fun getBookSiblingPrevious(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -370,6 +386,7 @@ class ReadListController(
)?.restrictUrl(!principal.user.isAdmin)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get next book in readlist", tags = [OpenApiConfiguration.TagNames.READLIST_BOOKS])
@GetMapping("{id}/books/{bookId}/next")
fun getBookSiblingNext(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -387,6 +404,7 @@ class ReadListController(
)?.restrictUrl(!principal.user.isAdmin)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get readlist read progress (Mihon)", description = "Mihon specific, due to how read progress is handled in Mihon.", tags = [OpenApiConfiguration.TagNames.MIHON])
@GetMapping("{id}/read-progress/tachiyomi")
fun getReadProgress(
@PathVariable id: String,
@ -396,6 +414,7 @@ class ReadListController(
readProgressDtoRepository.findProgressByReadList(readList.id, principal.user.id)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Update readlist read progress (Mihon)", description = "Mihon specific, due to how read progress is handled in Mihon.", tags = [OpenApiConfiguration.TagNames.MIHON])
@PutMapping("{id}/read-progress/tachiyomi")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun markReadProgressTachiyomi(
@ -415,6 +434,7 @@ class ReadListController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Download readlist", description = "Download the whole readlist as a ZIP file.", tags = [OpenApiConfiguration.TagNames.READLISTS])
@GetMapping("{id}/file", produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE])
@PreAuthorize("hasRole('FILE_DOWNLOAD')")
fun getReadListFile(

View file

@ -1,8 +1,11 @@
package org.gotson.komga.interfaces.api.rest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.tags.Tag
import org.gotson.komga.domain.persistence.ReferentialRepository
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
import org.gotson.komga.interfaces.api.rest.dto.AuthorDto
import org.gotson.komga.interfaces.api.rest.dto.toDto
@ -18,10 +21,13 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("api", produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = OpenApiConfiguration.TagNames.REFERENTIAL)
class ReferentialController(
private val referentialRepository: ReferentialRepository,
) {
@GetMapping("v1/authors")
@Deprecated("Use GET /v2/authors instead", ReplaceWith("getAuthors"))
@Operation(summary = "List authors", description = "Use GET /api/v2/authors instead. Deprecated since 1.20.0.", tags = [OpenApiConfiguration.TagNames.DEPRECATED])
fun getAuthorsV1(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "search", defaultValue = "") search: String,
@ -39,6 +45,7 @@ class ReferentialController(
@PageableWithoutSortAsQueryParam
@GetMapping("v2/authors")
@Operation(summary = "List authors", description = "Can be filtered by various criteria")
fun getAuthors(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "search", required = false) search: String?,
@ -69,17 +76,20 @@ class ReferentialController(
}
@GetMapping("v1/authors/names")
@Operation(summary = "List authors' names")
fun getAuthorsNames(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "search", defaultValue = "") search: String,
): List<String> = referentialRepository.findAllAuthorsNamesByName(search, principal.user.getAuthorizedLibraryIds(null))
@GetMapping("v1/authors/roles")
@Operation(summary = "List authors' roles")
fun getAuthorsRoles(
@AuthenticationPrincipal principal: KomgaPrincipal,
): List<String> = referentialRepository.findAllAuthorsRoles(principal.user.getAuthorizedLibraryIds(null))
@GetMapping("v1/genres")
@Operation(summary = "List genres", description = "Can be filtered by various criteria")
fun getGenres(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryId: String?,
@ -92,6 +102,7 @@ class ReferentialController(
}
@GetMapping("v1/sharing-labels")
@Operation(summary = "List sharing labels", description = "Can be filtered by various criteria")
fun getSharingLabels(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryId: String?,
@ -104,6 +115,7 @@ class ReferentialController(
}
@GetMapping("v1/tags")
@Operation(summary = "List tags", description = "Can be filtered by various criteria")
fun getTags(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryId: String?,
@ -116,6 +128,7 @@ class ReferentialController(
}
@GetMapping("v1/tags/book")
@Operation(summary = "List book tags", description = "Can be filtered by various criteria")
fun getBookTags(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "series_id", required = false) seriesId: String?,
@ -128,6 +141,7 @@ class ReferentialController(
}
@GetMapping("v1/tags/series")
@Operation(summary = "List series tags", description = "Can be filtered by various criteria")
fun getSeriesTags(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryId: String?,
@ -140,6 +154,7 @@ class ReferentialController(
}
@GetMapping("v1/languages")
@Operation(summary = "List languages", description = "Can be filtered by various criteria")
fun getLanguages(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryId: String?,
@ -152,6 +167,7 @@ class ReferentialController(
}
@GetMapping("v1/publishers")
@Operation(summary = "List publishers", description = "Can be filtered by various criteria")
fun getPublishers(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryId: String?,
@ -164,6 +180,7 @@ class ReferentialController(
}
@GetMapping("v1/age-ratings")
@Operation(summary = "List age ratings", description = "Can be filtered by various criteria")
fun getAgeRatings(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryId: String?,
@ -176,6 +193,7 @@ class ReferentialController(
}.map { it?.toString() ?: "None" }.toSet()
@GetMapping("v1/series/release-dates")
@Operation(summary = "List series release dates", description = "Can be filtered by various criteria")
fun getSeriesReleaseDates(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryId: String?,

View file

@ -1,7 +1,10 @@
package org.gotson.komga.interfaces.api.rest
import com.github.benmanes.caffeine.cache.Caffeine
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.interfaces.api.rest.dto.GithubReleaseDto
import org.gotson.komga.interfaces.api.rest.dto.ReleaseDto
import org.springframework.core.ParameterizedTypeReference
@ -21,6 +24,7 @@ private const val GITHUB_API = "https://api.github.com/repos/gotson/komga/releas
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("api/v1/releases", produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = OpenApiConfiguration.TagNames.RELEASES)
class ReleaseController(
webClientBuilder: WebClient.Builder,
) {
@ -34,7 +38,8 @@ class ReleaseController(
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
fun getAnnouncements(
@Operation(summary = "List releases")
fun getReleases(
@AuthenticationPrincipal principal: KomgaPrincipal,
): List<ReleaseDto> =
cache

View file

@ -1,6 +1,7 @@
package org.gotson.komga.interfaces.api.rest
import io.github.oshai.kotlinlogging.KotlinLogging
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
@ -26,6 +27,7 @@ import org.gotson.komga.infrastructure.jooq.UnpagedSorted
import org.gotson.komga.infrastructure.mediacontainer.ContentDetector
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
import org.gotson.komga.infrastructure.web.Authors
import org.gotson.komga.interfaces.api.persistence.SeriesDtoRepository
@ -77,6 +79,7 @@ class SeriesCollectionController(
private val thumbnailSeriesCollectionRepository: ThumbnailSeriesCollectionRepository,
private val eventPublisher: ApplicationEventPublisher,
) {
@Operation(summary = "List collections", tags = [OpenApiConfiguration.TagNames.COLLECTIONS])
@PageableWithoutSortAsQueryParam
@GetMapping
fun getAll(
@ -107,6 +110,7 @@ class SeriesCollectionController(
.map { it.toDto() }
}
@Operation(summary = "Get collection details", tags = [OpenApiConfiguration.TagNames.COLLECTIONS])
@GetMapping("{id}")
fun getOne(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -117,6 +121,7 @@ class SeriesCollectionController(
?.toDto()
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get collection's poster image", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
@GetMapping(value = ["{id}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE])
fun getCollectionThumbnail(
@ -131,6 +136,7 @@ class SeriesCollectionController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Get collection poster image", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
@GetMapping(value = ["{id}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE])
fun getCollectionThumbnailById(
@ -144,6 +150,7 @@ class SeriesCollectionController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "List collection's posters", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER])
@GetMapping(value = ["{id}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE])
fun getCollectionThumbnails(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -154,6 +161,7 @@ class SeriesCollectionController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Add collection poster", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER])
@PostMapping(value = ["{id}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@PreAuthorize("hasRole('ADMIN')")
fun addUserUploadedCollectionThumbnail(
@ -183,6 +191,7 @@ class SeriesCollectionController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Mark collection poster as selected", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER])
@PutMapping("{id}/thumbnails/{thumbnailId}/selected")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -199,6 +208,7 @@ class SeriesCollectionController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Delete collection poster", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER])
@DeleteMapping("{id}/thumbnails/{thumbnailId}")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -214,6 +224,7 @@ class SeriesCollectionController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Create collection", tags = [OpenApiConfiguration.TagNames.COLLECTIONS])
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
fun addOne(
@ -233,6 +244,7 @@ class SeriesCollectionController(
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message)
}
@Operation(summary = "Update collection", tags = [OpenApiConfiguration.TagNames.COLLECTIONS])
@PatchMapping("{id}")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ -256,6 +268,7 @@ class SeriesCollectionController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Delete collection", tags = [OpenApiConfiguration.TagNames.COLLECTIONS])
@DeleteMapping("{id}")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ -267,6 +280,7 @@ class SeriesCollectionController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "List collection's series", tags = [OpenApiConfiguration.TagNames.COLLECTION_SERIES])
@PageableWithoutSortAsQueryParam
@AuthorsAsQueryParam
@GetMapping("{id}/series")

View file

@ -46,6 +46,7 @@ import org.gotson.komga.infrastructure.jooq.UnpagedSorted
import org.gotson.komga.infrastructure.mediacontainer.ContentDetector
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
import org.gotson.komga.infrastructure.web.Authors
@ -119,6 +120,7 @@ class SeriesController(
private val thumbnailsSeriesRepository: ThumbnailSeriesRepository,
private val contentRestrictionChecker: ContentRestrictionChecker,
) {
@Operation(summary = "List series", description = "Use POST /api/v1/series/list instead. Deprecated since 1.19.0.", tags = [OpenApiConfiguration.TagNames.SERIES, OpenApiConfiguration.TagNames.DEPRECATED])
@Deprecated("use /v1/series/list instead")
@PageableAsQueryParam
@AuthorsAsQueryParam
@ -131,7 +133,6 @@ class SeriesController(
),
)
@GetMapping("v1/series")
@Operation(summary = "Use POST /api/v1/series/list instead")
fun getAllSeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "search", required = false) searchTerm: String? = null,
@ -222,6 +223,7 @@ class SeriesController(
.map { it.restrictUrl(!principal.user.isAdmin) }
}
@Operation(summary = "List series", tags = [OpenApiConfiguration.TagNames.SERIES])
@PageableAsQueryParam
@PostMapping("v1/series/list")
fun getSeriesList(
@ -252,6 +254,7 @@ class SeriesController(
.map { it.restrictUrl(!principal.user.isAdmin) }
}
@Operation(summary = "List series groups", description = "Use POST /api/v1/series/list/alphabetical-groups instead. Deprecated since 1.19.0.", tags = [OpenApiConfiguration.TagNames.SERIES, OpenApiConfiguration.TagNames.DEPRECATED])
@Deprecated("use /v1/series/list/alphabetical-groups instead")
@AuthorsAsQueryParam
@Parameters(
@ -263,7 +266,6 @@ class SeriesController(
),
)
@GetMapping("v1/series/alphabetical-groups")
@Operation(summary = "Use POST /api/v1/series/list/alphabetical-groups instead")
fun getAlphabeticalGroups(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "search", required = false) searchTerm: String?,
@ -334,13 +336,14 @@ class SeriesController(
return seriesDtoRepository.countByFirstCharacter(seriesSearch, SearchContext(principal.user))
}
@Operation(summary = "List series groups", description = "List series grouped by the first character of their sort title.", tags = [OpenApiConfiguration.TagNames.SERIES])
@PostMapping("v1/series/list/alphabetical-groups")
fun getSeriesListByAlphabeticalGroups(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestBody search: SeriesSearch,
): List<GroupCountDto> = seriesDtoRepository.countByFirstCharacter(search, SearchContext(principal.user))
@Operation(description = "Return recently added or updated series.")
@Operation(summary = "List latest series", description = "Return recently added or updated series.", tags = [OpenApiConfiguration.TagNames.SERIES])
@PageableWithoutSortAsQueryParam
@GetMapping("v1/series/latest")
fun getLatestSeries(
@ -379,7 +382,7 @@ class SeriesController(
).map { it.restrictUrl(!principal.user.isAdmin) }
}
@Operation(description = "Return newly added series.")
@Operation(summary = "List new series", description = "Return newly added series.", tags = [OpenApiConfiguration.TagNames.SERIES])
@PageableWithoutSortAsQueryParam
@GetMapping("v1/series/new")
fun getNewSeries(
@ -418,7 +421,7 @@ class SeriesController(
).map { it.restrictUrl(!principal.user.isAdmin) }
}
@Operation(description = "Return recently updated series, but not newly added ones.")
@Operation(summary = "List updated series", description = "Return recently updated series, but not newly added ones.", tags = [OpenApiConfiguration.TagNames.SERIES])
@PageableWithoutSortAsQueryParam
@GetMapping("v1/series/updated")
fun getUpdatedSeries(
@ -457,6 +460,7 @@ class SeriesController(
).map { it.restrictUrl(!principal.user.isAdmin) }
}
@Operation(summary = "Get series details", tags = [OpenApiConfiguration.TagNames.SERIES])
@GetMapping("v1/series/{seriesId}")
fun getOneSeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -467,6 +471,7 @@ class SeriesController(
it.restrictUrl(!principal.user.isAdmin)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(summary = "Get series' poster image", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
@GetMapping(value = ["v1/series/{seriesId}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE])
fun getSeriesDefaultThumbnail(
@ -479,6 +484,7 @@ class SeriesController(
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Get series poster image", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER])
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
@GetMapping(value = ["v1/series/{seriesId}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE])
fun getSeriesThumbnailById(
@ -492,6 +498,7 @@ class SeriesController(
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "List series posters", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER])
@GetMapping(value = ["v1/series/{seriesId}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE])
fun getSeriesThumbnails(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -504,6 +511,7 @@ class SeriesController(
.map { it.toDto() }
}
@Operation(summary = "Add series poster", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER])
@PostMapping(value = ["v1/series/{seriesId}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@PreAuthorize("hasRole('ADMIN')")
fun postUserUploadedSeriesThumbnail(
@ -532,6 +540,7 @@ class SeriesController(
).toDto()
}
@Operation(summary = "Mark series poster as selected", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER])
@PutMapping("v1/series/{seriesId}/thumbnails/{thumbnailId}/selected")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -546,6 +555,7 @@ class SeriesController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "Delete series poster", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER])
@DeleteMapping("v1/series/{seriesId}/thumbnails/{thumbnailId}")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -563,11 +573,11 @@ class SeriesController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@Operation(summary = "List series' books", description = "Use POST /api/v1/books/list instead. Deprecated since 1.19.0.", tags = [OpenApiConfiguration.TagNames.SERIES, OpenApiConfiguration.TagNames.DEPRECATED])
@Deprecated("use /v1/books/list instead")
@PageableAsQueryParam
@AuthorsAsQueryParam
@GetMapping("v1/series/{seriesId}/books")
@Operation(summary = "Use POST /api/v1/books/list instead")
fun getAllBooksBySeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable(name = "seriesId") seriesId: String,
@ -618,6 +628,7 @@ class SeriesController(
).map { it.restrictUrl(!principal.user.isAdmin) }
}
@Operation(summary = "List series' collections", tags = [OpenApiConfiguration.TagNames.SERIES])
@GetMapping("v1/series/{seriesId}/collections")
fun getAllCollectionsBySeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -630,6 +641,7 @@ class SeriesController(
.map { it.toDto() }
}
@Operation(summary = "Analyze series", tags = [OpenApiConfiguration.TagNames.SERIES])
@PostMapping("v1/series/{seriesId}/analyze")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -639,6 +651,7 @@ class SeriesController(
taskEmitter.analyzeBook(bookRepository.findAllBySeriesId(seriesId), HIGH_PRIORITY)
}
@Operation(summary = "Refresh series metadata", tags = [OpenApiConfiguration.TagNames.SERIES])
@PostMapping("v1/series/{seriesId}/metadata/refresh")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
@ -651,6 +664,7 @@ class SeriesController(
taskEmitter.refreshSeriesLocalArtwork(seriesId, priority = HIGH_PRIORITY)
}
@Operation(summary = "Update series metadata", tags = [OpenApiConfiguration.TagNames.SERIES])
@PatchMapping("v1/series/{seriesId}/metadata")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ -725,7 +739,7 @@ class SeriesController(
seriesRepository.findByIdOrNull(seriesId)?.let { eventPublisher.publishEvent(DomainEvent.SeriesUpdated(it)) }
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@Operation(description = "Mark all book for series as read")
@Operation(summary = "Mark series as read", description = "Mark all book for series as read", tags = [OpenApiConfiguration.TagNames.SERIES])
@PostMapping("v1/series/{seriesId}/read-progress")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun markAsRead(
@ -737,7 +751,7 @@ class SeriesController(
seriesLifecycle.markReadProgressCompleted(seriesId, principal.user)
}
@Operation(description = "Mark all book for series as unread")
@Operation(summary = "Mark series as unread", description = "Mark all book for series as unread", tags = [OpenApiConfiguration.TagNames.SERIES])
@DeleteMapping("v1/series/{seriesId}/read-progress")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun markAsUnread(
@ -749,6 +763,7 @@ class SeriesController(
seriesLifecycle.deleteReadProgress(seriesId, principal.user)
}
@Operation(summary = "Get series read progress (Mihon)", description = "Mihon specific, due to how read progress is handled in Mihon.", tags = [OpenApiConfiguration.TagNames.MIHON])
@GetMapping("v2/series/{seriesId}/read-progress/tachiyomi")
fun getReadProgressTachiyomiV2(
@PathVariable seriesId: String,
@ -759,6 +774,7 @@ class SeriesController(
return readProgressDtoRepository.findProgressV2BySeries(seriesId, principal.user.id)
}
@Operation(summary = "Update series read progress (Mihon)", description = "Mihon specific, due to how read progress is handled in Mihon.", tags = [OpenApiConfiguration.TagNames.MIHON])
@PutMapping("v2/series/{seriesId}/read-progress/tachiyomi")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun markReadProgressTachiyomiV2(
@ -781,6 +797,7 @@ class SeriesController(
}
}
@Operation(summary = "Download series", description = "Download the whole series as a ZIP file.", tags = [OpenApiConfiguration.TagNames.SERIES])
@GetMapping("v1/series/{seriesId}/file", produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE])
@PreAuthorize("hasRole('FILE_DOWNLOAD')")
fun getSeriesFile(
@ -828,6 +845,7 @@ class SeriesController(
.body(streamingResponse)
}
@Operation(summary = "Delete series files", description = "Delete all of the series' books files on disk.", tags = [OpenApiConfiguration.TagNames.SERIES])
@DeleteMapping("v1/series/{seriesId}/file")
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)

View file

@ -1,8 +1,12 @@
package org.gotson.komga.interfaces.api.rest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.gotson.komga.infrastructure.kobo.KepubConverter
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.web.WebServerEffectiveSettings
import org.gotson.komga.interfaces.api.rest.dto.SettingMultiSource
import org.gotson.komga.interfaces.api.rest.dto.SettingsDto
@ -24,6 +28,7 @@ import kotlin.time.Duration.Companion.days
@RestController
@RequestMapping(value = ["api/v1/settings"], produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = OpenApiConfiguration.TagNames.SERVER_SETTINGS)
class SettingsController(
private val komgaSettingsProvider: KomgaSettingsProvider,
@Value("\${server.port:#{null}}") private val configServerPort: Int?,
@ -32,6 +37,7 @@ class SettingsController(
private val kepubConverter: KepubConverter,
) {
@GetMapping
@Operation(summary = "Retrieve server settings")
fun getSettings(): SettingsDto =
SettingsDto(
komgaSettingsProvider.deleteEmptyCollections,
@ -48,8 +54,10 @@ class SettingsController(
@PatchMapping
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Update server settings", description = "You can omit fields you don't want to update")
fun updateSettings(
@Valid @RequestBody
@Parameter(description = "Fields to update. You can omit fields you don't want to update.")
newSettings: SettingsUpdateDto,
) {
newSettings.deleteEmptyCollections?.let { komgaSettingsProvider.deleteEmptyCollections = it }

View file

@ -1,7 +1,10 @@
package org.gotson.komga.interfaces.api.rest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.gotson.komga.domain.persistence.SyncPointRepository
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.core.annotation.AuthenticationPrincipal
@ -13,11 +16,16 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("api/v1/syncpoints", produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = OpenApiConfiguration.TagNames.SYNCPOINTS)
class SyncPointController(
private val syncPointRepository: SyncPointRepository,
) {
@DeleteMapping("me")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(
summary = "Delete all sync points",
description = "If an API Key ID is passed, deletes only the sync points associated with that API Key. Deleting sync points will allow a Kobo to sync from scratch upon the next sync.",
)
fun deleteMySyncPointsByApiKey(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(required = false, name = "key_id") keyIds: Collection<String>?,

View file

@ -1,6 +1,9 @@
package org.gotson.komga.interfaces.api.rest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.gotson.komga.application.tasks.TasksRepository
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.access.prepost.PreAuthorize
@ -11,11 +14,13 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = OpenApiConfiguration.TagNames.TASKS)
class TaskController(
private val tasksRepository: TasksRepository,
) {
@DeleteMapping("api/v1/tasks")
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Clear task queue", description = "Cancel all tasks queued")
fun emptyTaskQueue(): Int = tasksRepository.deleteAllWithoutOwner()
}

View file

@ -2,6 +2,8 @@ package org.gotson.komga.interfaces.api.rest
import com.jakewharton.byteunits.BinaryByteUnit
import io.github.oshai.kotlinlogging.KotlinLogging
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.gotson.komga.domain.model.CodedException
import org.gotson.komga.domain.model.MediaNotReadyException
import org.gotson.komga.domain.model.MediaProfile
@ -9,6 +11,7 @@ import org.gotson.komga.domain.model.TransientBook
import org.gotson.komga.domain.persistence.TransientBookRepository
import org.gotson.komga.domain.service.BookAnalyzer
import org.gotson.komga.domain.service.TransientBookLifecycle
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration
import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault
import org.gotson.komga.infrastructure.web.toFilePath
import org.gotson.komga.interfaces.api.rest.dto.PageDto
@ -31,12 +34,14 @@ private val logger = KotlinLogging.logger {}
@RestController
@RequestMapping("api/v1/transient-books", produces = [MediaType.APPLICATION_JSON_VALUE])
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = OpenApiConfiguration.TagNames.BOOK_IMPORT)
class TransientBooksController(
private val transientBookLifecycle: TransientBookLifecycle,
private val transientBookRepository: TransientBookRepository,
private val bookAnalyzer: BookAnalyzer,
) {
@PostMapping
@Operation(summary = "Scan folder for transient books", description = "Scan provided folder for transient books.")
fun scanForTransientBooks(
@RequestBody request: ScanRequestDto,
): List<TransientBookDto> =
@ -50,6 +55,7 @@ class TransientBooksController(
}
@PostMapping("{id}/analyze")
@Operation(summary = "Analyze transient book")
fun analyze(
@PathVariable id: String,
): TransientBookDto =
@ -61,6 +67,7 @@ class TransientBooksController(
value = ["{id}/pages/{pageNumber}"],
produces = [MediaType.ALL_VALUE],
)
@Operation(summary = "Get transient book page")
fun getSourcePage(
@PathVariable id: String,
@PathVariable pageNumber: Int,

View file

@ -1,6 +1,7 @@
package org.gotson.komga.interfaces.api.rest
import io.github.oshai.kotlinlogging.KotlinLogging
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import jakarta.validation.Valid
import org.gotson.komga.domain.model.AgeRestriction
@ -14,6 +15,7 @@ import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.service.KomgaUserLifecycle
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames
import org.gotson.komga.interfaces.api.rest.dto.ApiKeyDto
import org.gotson.komga.interfaces.api.rest.dto.ApiKeyRequestDto
import org.gotson.komga.interfaces.api.rest.dto.AuthenticationActivityDto
@ -59,12 +61,14 @@ class UserController(
private val demo = env.activeProfiles.contains("demo")
@GetMapping("me")
@Operation(summary = "Retrieve current user", tags = [TagNames.CURRENT_USER])
fun getMe(
@AuthenticationPrincipal principal: KomgaPrincipal,
): UserDto = principal.toDto()
@PatchMapping("me/password")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Update current user's password", tags = [TagNames.CURRENT_USER])
fun updateMyPassword(
@AuthenticationPrincipal principal: KomgaPrincipal,
@Valid @RequestBody
@ -78,11 +82,13 @@ class UserController(
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "List users", tags = [TagNames.USERS])
fun getAll(): List<UserDto> = userRepository.findAll().map { it.toDto() }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Create user", tags = [TagNames.USERS])
fun addOne(
@Valid @RequestBody
newUser: UserCreationDto,
@ -96,6 +102,7 @@ class UserController(
@DeleteMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ADMIN') and #principal.user.id != #id")
@Operation(summary = "Delete user", tags = [TagNames.USERS])
fun delete(
@PathVariable id: String,
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -108,6 +115,7 @@ class UserController(
@PatchMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ADMIN') and #principal.user.id != #id")
@Operation(summary = "Update user", tags = [TagNames.USERS])
fun updateUser(
@PathVariable id: String,
@Valid @RequestBody
@ -162,6 +170,7 @@ class UserController(
@PatchMapping("{id}/password")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ADMIN') or #principal.user.id == #id")
@Operation(summary = "Update user's password", tags = [TagNames.USERS])
fun updatePassword(
@PathVariable id: String,
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -176,6 +185,7 @@ class UserController(
@GetMapping("me/authentication-activity")
@PageableAsQueryParam
@Operation(summary = "Retrieve authentication activity for the current user", tags = [TagNames.CURRENT_USER])
fun getMyAuthenticationActivity(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
@ -204,6 +214,7 @@ class UserController(
@GetMapping("authentication-activity")
@PageableAsQueryParam
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Retrieve authentication activity", tags = [TagNames.USERS])
fun getAuthenticationActivity(
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
@Parameter(hidden = true) page: Pageable,
@ -229,6 +240,7 @@ class UserController(
@GetMapping("{id}/authentication-activity/latest")
@PreAuthorize("hasRole('ADMIN') or #principal.user.id == #id")
@Operation(summary = "Retrieve latest authentication activity for a user", tags = [TagNames.USERS])
fun getLatestAuthenticationActivityForUser(
@PathVariable id: String,
@AuthenticationPrincipal principal: KomgaPrincipal,
@ -240,6 +252,7 @@ class UserController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@GetMapping("me/api-keys")
@Operation(summary = "Retrieve API keys", tags = [TagNames.API_KEYS])
fun getApiKeys(
@AuthenticationPrincipal principal: KomgaPrincipal,
): Collection<ApiKeyDto> {
@ -248,6 +261,7 @@ class UserController(
}
@PostMapping("me/api-keys")
@Operation(summary = "Create API key", tags = [TagNames.API_KEYS])
fun createApiKey(
@AuthenticationPrincipal principal: KomgaPrincipal,
@Valid @RequestBody apiKeyRequest: ApiKeyRequestDto,
@ -263,6 +277,7 @@ class UserController(
@DeleteMapping("me/api-keys/{keyId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Delete API key", tags = [TagNames.API_KEYS])
fun deleteApiKey(
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable keyId: String,

View file

@ -94,8 +94,6 @@ management:
step: 24h
springdoc:
swagger-ui:
groups-order: desc
operations-sorter: alpha
disable-swagger-default-url: true
tags-sorter: alpha
paths-to-match: "/api/**"
writer-with-order-by-keys: true