fix: use sort-order position as fallback volume for book matching
When books have no volume number, use their 1-based position in the backoffice sort order (volume ASC NULLS LAST, natural title sort) as effective volume for matching against external provider books. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -829,12 +829,17 @@ 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
|
// Pre-fetch all local books for this series, sorted like the backoffice
|
||||||
|
// (volume ASC NULLS LAST, then natural title sort)
|
||||||
let local_books: Vec<(Uuid, Option<i32>, String)> = sqlx::query_as(
|
let local_books: Vec<(Uuid, Option<i32>, String)> = sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id, volume, title FROM books
|
SELECT id, volume, title FROM books
|
||||||
WHERE library_id = $1
|
WHERE library_id = $1
|
||||||
AND COALESCE(NULLIF(series, ''), 'unclassified') = $2
|
AND COALESCE(NULLIF(series, ''), 'unclassified') = $2
|
||||||
|
ORDER BY volume NULLS LAST,
|
||||||
|
REGEXP_REPLACE(LOWER(title), '[0-9].*$', ''),
|
||||||
|
COALESCE((REGEXP_MATCH(LOWER(title), '\d+'))[1]::int, 0),
|
||||||
|
title ASC
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(library_id)
|
.bind(library_id)
|
||||||
@@ -842,24 +847,30 @@ async fn sync_books_metadata(
|
|||||||
.fetch_all(&state.pool)
|
.fetch_all(&state.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Build effective position for each local book: use volume if set, otherwise 1-based sort order
|
||||||
|
let local_books_with_pos: Vec<(Uuid, i32, String)> = local_books
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, (id, vol, title))| (*id, vol.unwrap_or((idx + 1) as i32), title.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Track which local books have already been matched to avoid double-matching
|
// Track which local books have already been matched to avoid double-matching
|
||||||
let mut matched_local_ids = std::collections::HashSet::new();
|
let mut matched_local_ids = std::collections::HashSet::new();
|
||||||
|
|
||||||
for book in &books {
|
for (ext_idx, book) in books.iter().enumerate() {
|
||||||
// Strategy 1: Match by volume number
|
// Effective volume for the external book: provider volume_number, or 1-based position
|
||||||
let mut local_book_id: Option<Uuid> = if let Some(vol) = book.volume_number {
|
let ext_vol = book.volume_number.unwrap_or((ext_idx + 1) as i32);
|
||||||
local_books
|
|
||||||
|
// Strategy 1: Match by effective volume number
|
||||||
|
let mut local_book_id: Option<Uuid> = local_books_with_pos
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(id, v, _)| *v == Some(vol) && !matched_local_ids.contains(id))
|
.find(|(id, v, _)| *v == ext_vol && !matched_local_ids.contains(id))
|
||||||
.map(|(id, _, _)| *id)
|
.map(|(id, _, _)| *id);
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Strategy 2: External title contained in local title or vice-versa (case-insensitive)
|
// Strategy 2: External title contained in local title or vice-versa (case-insensitive)
|
||||||
if local_book_id.is_none() {
|
if local_book_id.is_none() {
|
||||||
let ext_title_lower = book.title.to_lowercase();
|
let ext_title_lower = book.title.to_lowercase();
|
||||||
local_book_id = local_books.iter().find(|(id, _, local_title)| {
|
local_book_id = local_books_with_pos.iter().find(|(id, _, local_title)| {
|
||||||
if matched_local_ids.contains(id) {
|
if matched_local_ids.contains(id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user