fix: re-matching automatique des métadonnées externes après scan/import
Quand les métadonnées externes sont récupérées avant que les livres n'existent localement, le book_id reste NULL et les livres apparaissent comme "manquants" alors qu'ils sont présents. - Ajoute rematch_unlinked_books() qui associe les external_book_metadata non liées aux livres locaux par correspondance de volume - Appelé automatiquement à la fin de refresh_link() (API) - Appelé après chaque scan terminé dans l'indexer (job.rs) - Testé sur Dragon Ball : 47/85 external books rematched, 43 restants correspondent aux tomes Dragon Ball Z non présents localement Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -643,6 +643,10 @@ pub(crate) async fn refresh_link(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Re-match any external books that couldn't be matched during insert
|
||||||
|
// (e.g., metadata was fetched before books were imported)
|
||||||
|
let _ = rematch_unlinked_books(pool, library_id).await;
|
||||||
|
|
||||||
let has_changes = !series_changes.is_empty() || !book_changes.is_empty();
|
let has_changes = !series_changes.is_empty() || !book_changes.is_empty();
|
||||||
|
|
||||||
Ok(SeriesRefreshResult {
|
Ok(SeriesRefreshResult {
|
||||||
@@ -947,3 +951,40 @@ async fn sync_book_with_diff(
|
|||||||
|
|
||||||
Ok(diffs)
|
Ok(diffs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Re-match external_book_metadata rows that have book_id IS NULL
|
||||||
|
/// by joining on volume number with local books in the same series.
|
||||||
|
/// Called after scans/imports to fix metadata that was fetched before books existed.
|
||||||
|
pub async fn rematch_unlinked_books(pool: &PgPool, library_id: Uuid) -> Result<i64, String> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE external_book_metadata ebm
|
||||||
|
SET book_id = matched.book_id
|
||||||
|
FROM (
|
||||||
|
SELECT DISTINCT ON (ebm2.id)
|
||||||
|
ebm2.id AS ebm_id,
|
||||||
|
b.id AS book_id
|
||||||
|
FROM external_book_metadata ebm2
|
||||||
|
JOIN external_metadata_links eml ON eml.id = ebm2.link_id
|
||||||
|
JOIN books b ON b.library_id = eml.library_id
|
||||||
|
AND LOWER(COALESCE(NULLIF(b.series, ''), 'unclassified')) = LOWER(eml.series_name)
|
||||||
|
AND b.volume = ebm2.volume_number
|
||||||
|
WHERE eml.library_id = $1
|
||||||
|
AND ebm2.book_id IS NULL
|
||||||
|
AND ebm2.volume_number IS NOT NULL
|
||||||
|
AND eml.status = 'approved'
|
||||||
|
) matched
|
||||||
|
WHERE ebm.id = matched.ebm_id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(library_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let count = result.rows_affected() as i64;
|
||||||
|
if count > 0 {
|
||||||
|
info!("[METADATA] Re-matched {count} unlinked external books for library {library_id}");
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1096,7 +1096,7 @@ pub async fn update_series(
|
|||||||
)]
|
)]
|
||||||
pub async fn delete_series(
|
pub async fn delete_series(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Extension(user): Extension<AuthUser>,
|
Extension(_user): Extension<AuthUser>,
|
||||||
Path((library_id, name)): Path<(Uuid, String)>,
|
Path((library_id, name)): Path<(Uuid, String)>,
|
||||||
) -> Result<Json<crate::responses::DeletedResponse>, ApiError> {
|
) -> Result<Json<crate::responses::DeletedResponse>, ApiError> {
|
||||||
use stripstream_core::paths::remap_libraries_path;
|
use stripstream_core::paths::remap_libraries_path;
|
||||||
|
|||||||
@@ -5,6 +5,44 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{analyzer, converter, scanner, AppState};
|
use crate::{analyzer, converter, scanner, AppState};
|
||||||
|
|
||||||
|
/// Re-match external_book_metadata with book_id IS NULL by volume number.
|
||||||
|
async fn rematch_unlinked_books(pool: &PgPool, library_id: Uuid) {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE external_book_metadata ebm
|
||||||
|
SET book_id = matched.book_id
|
||||||
|
FROM (
|
||||||
|
SELECT DISTINCT ON (ebm2.id)
|
||||||
|
ebm2.id AS ebm_id,
|
||||||
|
b.id AS book_id
|
||||||
|
FROM external_book_metadata ebm2
|
||||||
|
JOIN external_metadata_links eml ON eml.id = ebm2.link_id
|
||||||
|
JOIN books b ON b.library_id = eml.library_id
|
||||||
|
AND LOWER(COALESCE(NULLIF(b.series, ''), 'unclassified')) = LOWER(eml.series_name)
|
||||||
|
AND b.volume = ebm2.volume_number
|
||||||
|
WHERE eml.library_id = $1
|
||||||
|
AND ebm2.book_id IS NULL
|
||||||
|
AND ebm2.volume_number IS NOT NULL
|
||||||
|
AND eml.status = 'approved'
|
||||||
|
) matched
|
||||||
|
WHERE ebm.id = matched.ebm_id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(library_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(r) if r.rows_affected() > 0 => {
|
||||||
|
info!("[METADATA] Re-matched {} unlinked external books for library {}", r.rows_affected(), library_id);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("[METADATA] Failed to rematch unlinked books: {e}");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn cleanup_stale_jobs(pool: &PgPool) -> Result<()> {
|
pub async fn cleanup_stale_jobs(pool: &PgPool) -> Result<()> {
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -379,6 +417,12 @@ pub async fn process_job(
|
|||||||
|
|
||||||
analyzer::analyze_library_books(state, job_id, target_library_id, false).await?;
|
analyzer::analyze_library_books(state, job_id, target_library_id, false).await?;
|
||||||
|
|
||||||
|
// Re-match external book metadata that couldn't be linked during initial metadata fetch
|
||||||
|
// (e.g., metadata was fetched before books were scanned)
|
||||||
|
if let Some(lib_id) = target_library_id {
|
||||||
|
rematch_unlinked_books(&state.pool, lib_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"UPDATE index_jobs SET status = 'success', finished_at = NOW(), progress_percent = 100, current_file = NULL WHERE id = $1",
|
"UPDATE index_jobs SET status = 'success', finished_at = NOW(), progress_percent = 100, current_file = NULL WHERE id = $1",
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user