Files
stripstream-librarian/apps/api/src/openapi.rs
Froidefond Julien c9ccf5cd90 feat: add external metadata sync system with multiple providers
Add a complete metadata synchronization system allowing users to search
and sync series/book metadata from external providers (Google Books,
Open Library, ComicVine, AniList, Bédéthèque). Each library can use a
different provider. Matching requires manual approval with detailed sync
reports showing what was updated or skipped (locked fields protection).

Key changes:
- DB migrations: external_metadata_links, external_book_metadata tables,
  library metadata_provider column, locked_fields, total_volumes, book
  metadata fields (summary, isbn, publish_date)
- Rust API: MetadataProvider trait + 5 provider implementations,
  7 metadata endpoints (search, match, approve, reject, links, missing,
  delete), sync report system, provider language preference support
- Backoffice: MetadataSearchModal, ProviderIcon, SafeHtml components,
  settings UI for provider/language config, enriched book detail page,
  edit forms with locked fields support, API proxy routes
- OpenAPI/Swagger documentation for all new endpoints and schemas

Closes #3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 14:59:24 +01:00

197 lines
7.4 KiB
Rust

use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
paths(
crate::books::list_books,
crate::books::get_book,
crate::reading_progress::get_reading_progress,
crate::reading_progress::update_reading_progress,
crate::reading_progress::mark_series_read,
crate::books::get_thumbnail,
crate::books::list_series,
crate::books::list_all_series,
crate::books::ongoing_series,
crate::books::ongoing_books,
crate::books::convert_book,
crate::books::update_book,
crate::books::get_series_metadata,
crate::books::update_series,
crate::pages::get_page,
crate::search::search_books,
crate::index_jobs::enqueue_rebuild,
crate::thumbnails::start_thumbnails_rebuild,
crate::thumbnails::start_thumbnails_regenerate,
crate::index_jobs::list_index_jobs,
crate::index_jobs::get_active_jobs,
crate::index_jobs::get_job_details,
crate::index_jobs::stream_job_progress,
crate::index_jobs::get_job_errors,
crate::index_jobs::cancel_job,
crate::index_jobs::list_folders,
crate::libraries::list_libraries,
crate::libraries::create_library,
crate::libraries::delete_library,
crate::libraries::scan_library,
crate::libraries::update_monitoring,
crate::tokens::list_tokens,
crate::tokens::create_token,
crate::tokens::revoke_token,
crate::tokens::delete_token,
crate::stats::get_stats,
crate::settings::get_settings,
crate::settings::get_setting,
crate::settings::update_setting,
crate::settings::clear_cache,
crate::settings::get_cache_stats,
crate::settings::get_thumbnail_stats,
crate::metadata::search_metadata,
crate::metadata::create_metadata_match,
crate::metadata::approve_metadata,
crate::metadata::reject_metadata,
crate::metadata::get_metadata_links,
crate::metadata::get_missing_books,
crate::metadata::delete_metadata_link,
),
components(
schemas(
crate::books::ListBooksQuery,
crate::books::BookItem,
crate::books::BooksPage,
crate::books::BookDetails,
crate::reading_progress::ReadingProgressResponse,
crate::reading_progress::UpdateReadingProgressRequest,
crate::reading_progress::MarkSeriesReadRequest,
crate::reading_progress::MarkSeriesReadResponse,
crate::books::SeriesItem,
crate::books::SeriesPage,
crate::books::ListAllSeriesQuery,
crate::books::OngoingQuery,
crate::books::UpdateBookRequest,
crate::books::SeriesMetadata,
crate::books::UpdateSeriesRequest,
crate::books::UpdateSeriesResponse,
crate::pages::PageQuery,
crate::search::SearchQuery,
crate::search::SearchResponse,
crate::search::SeriesHit,
crate::index_jobs::RebuildRequest,
crate::thumbnails::ThumbnailsRebuildRequest,
crate::index_jobs::IndexJobResponse,
crate::index_jobs::IndexJobDetailResponse,
crate::index_jobs::JobErrorResponse,
crate::index_jobs::ProgressEvent,
crate::index_jobs::FolderItem,
crate::libraries::LibraryResponse,
crate::libraries::CreateLibraryRequest,
crate::libraries::UpdateMonitoringRequest,
crate::tokens::CreateTokenRequest,
crate::tokens::TokenResponse,
crate::tokens::CreatedTokenResponse,
crate::settings::UpdateSettingRequest,
crate::settings::ClearCacheResponse,
crate::settings::CacheStats,
crate::settings::ThumbnailStats,
crate::stats::StatsResponse,
crate::stats::StatsOverview,
crate::stats::ReadingStatusStats,
crate::stats::FormatCount,
crate::stats::LanguageCount,
crate::stats::LibraryStats,
crate::stats::TopSeries,
crate::stats::MonthlyAdditions,
crate::metadata::ApproveRequest,
crate::metadata::ApproveResponse,
crate::metadata::SyncReport,
crate::metadata::SeriesSyncReport,
crate::metadata::BookSyncReport,
crate::metadata::FieldChange,
crate::metadata::MetadataSearchRequest,
crate::metadata::SeriesCandidateDto,
crate::metadata::MetadataMatchRequest,
crate::metadata::ExternalMetadataLinkDto,
crate::metadata::MissingBooksDto,
crate::metadata::MissingBookItem,
ErrorResponse,
)
),
security(
("Bearer" = [])
),
tags(
(name = "books", description = "Read-only endpoints for browsing and searching books"),
(name = "reading-progress", description = "Reading progress tracking per book"),
(name = "libraries", description = "Library management endpoints (Admin only)"),
(name = "indexing", description = "Search index management and job control (Admin only)"),
(name = "tokens", description = "API token management (Admin only)"),
(name = "settings", description = "Application settings and cache management (Admin only)"),
),
modifiers(&SecurityAddon)
)]
pub struct ApiDoc;
pub struct SecurityAddon;
impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(components) = openapi.components.as_mut() {
components.add_security_scheme(
"Bearer",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT")
.description(Some(
"Enter your API Bearer token (format: stl_<prefix>_<secret>)",
))
.build(),
),
);
}
}
}
#[derive(utoipa::ToSchema)]
pub struct ErrorResponse {
#[allow(dead_code)]
pub error: String,
}
#[cfg(test)]
mod tests {
use super::*;
use utoipa::OpenApi;
#[test]
fn test_openapi_generation() {
let api_doc = ApiDoc::openapi();
let json = api_doc
.to_pretty_json()
.expect("Failed to serialize OpenAPI");
// Check that all $ref targets exist in components/schemas
let doc: serde_json::Value =
serde_json::from_str(&json).expect("OpenAPI JSON should be valid");
let empty = serde_json::Map::new();
let schemas = doc["components"]["schemas"]
.as_object()
.unwrap_or(&empty);
let prefix = "#/components/schemas/";
let mut broken: Vec<String> = Vec::new();
for part in json.split(prefix).skip(1) {
if let Some(name) = part.split('"').next() {
if !schemas.contains_key(name) {
broken.push(name.to_string());
}
}
}
broken.dedup();
assert!(broken.is_empty(), "Unresolved schema refs: {:?}", broken);
// Save to file for inspection
std::fs::write("/tmp/openapi.json", &json).expect("Failed to write file");
println!("OpenAPI JSON saved to /tmp/openapi.json");
}
}