fix: improve book matching in metadata sync with bidirectional title search

Pre-fetch all local books in one query instead of N queries per external
book. Match by volume number first, then bidirectional title containment
(external in local OR local in external). Track matched IDs to prevent
double-matching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 15:12:36 +01:00
parent ac13f53124
commit f75d795215

View File

@@ -829,48 +829,48 @@ async fn sync_books_metadata(
let mut matched_count: i64 = 0; let mut matched_count: i64 = 0;
let mut book_reports: Vec<BookSyncReport> = Vec::new(); let mut book_reports: Vec<BookSyncReport> = Vec::new();
// Pre-fetch all local books for this series to enable flexible matching
let local_books: Vec<(Uuid, Option<i32>, String)> = sqlx::query_as(
r#"
SELECT id, volume, title FROM books
WHERE library_id = $1
AND COALESCE(NULLIF(series, ''), 'unclassified') = $2
"#,
)
.bind(library_id)
.bind(series_name)
.fetch_all(&state.pool)
.await?;
// Track which local books have already been matched to avoid double-matching
let mut matched_local_ids = std::collections::HashSet::new();
for book in &books { for book in &books {
// Try to match with local book by volume_number first, then title // Strategy 1: Match by volume number
let local_book_id: Option<Uuid> = if let Some(vol) = book.volume_number { let mut local_book_id: Option<Uuid> = if let Some(vol) = book.volume_number {
sqlx::query_scalar( local_books
r#" .iter()
SELECT id FROM books .find(|(id, v, _)| *v == Some(vol) && !matched_local_ids.contains(id))
WHERE library_id = $1 .map(|(id, _, _)| *id)
AND COALESCE(NULLIF(series, ''), 'unclassified') = $2
AND volume = $3
LIMIT 1
"#,
)
.bind(library_id)
.bind(series_name)
.bind(vol)
.fetch_optional(&state.pool)
.await?
} else { } else {
None None
}; };
let local_book_id = match local_book_id { // Strategy 2: External title contained in local title or vice-versa (case-insensitive)
Some(id) => Some(id), if local_book_id.is_none() {
None => { let ext_title_lower = book.title.to_lowercase();
// Try matching by title local_book_id = local_books.iter().find(|(id, _, local_title)| {
let pattern = format!("%{}%", book.title); if matched_local_ids.contains(id) {
sqlx::query_scalar( return false;
r#" }
SELECT id FROM books let local_lower = local_title.to_lowercase();
WHERE library_id = $1 local_lower.contains(&ext_title_lower) || ext_title_lower.contains(&local_lower)
AND COALESCE(NULLIF(series, ''), 'unclassified') = $2 }).map(|(id, _, _)| *id);
AND title ILIKE $3 }
LIMIT 1
"#, if let Some(id) = local_book_id {
) matched_local_ids.insert(id);
.bind(library_id) }
.bind(series_name)
.bind(&pattern)
.fetch_optional(&state.pool)
.await?
}
};
sqlx::query( sqlx::query(
r#" r#"