fix: prevent scanner from recreating renamed series
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 47s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 47s
When a user renames a series via the UI, the scanner was using the filesystem directory name to overwrite the DB series name, effectively undoing the rename. This adds an original_name column to series_metadata that tracks the filesystem-derived name, so the scanner can map it back to the user-chosen name. The migration also back-fills existing renamed series by comparing book file paths with DB series names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1015,10 +1015,29 @@ pub async fn update_series(
|
||||
.filter(|a| !a.is_empty())
|
||||
.collect();
|
||||
let locked_fields = body.locked_fields.clone().unwrap_or(serde_json::json!({}));
|
||||
|
||||
// When renaming, preserve the filesystem-derived original name so the scanner
|
||||
// can map files back to the renamed series instead of recreating the old one.
|
||||
let is_rename = name != "unclassified" && new_name != name;
|
||||
let original_name: Option<String> = if is_rename {
|
||||
// Check if the old metadata already has an original_name (chained renames: A→B→C)
|
||||
let existing_original: Option<Option<String>> = sqlx::query_scalar(
|
||||
"SELECT original_name FROM series_metadata WHERE library_id = $1 AND name = $2"
|
||||
)
|
||||
.bind(library_id)
|
||||
.bind(&name)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
// Use existing original_name if set, otherwise use the old name itself
|
||||
Some(existing_original.flatten().unwrap_or_else(|| name.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO series_metadata (library_id, name, authors, description, publishers, start_year, total_volumes, status, locked_fields, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())
|
||||
INSERT INTO series_metadata (library_id, name, authors, description, publishers, start_year, total_volumes, status, locked_fields, original_name, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
|
||||
ON CONFLICT (library_id, name) DO UPDATE
|
||||
SET authors = EXCLUDED.authors,
|
||||
description = EXCLUDED.description,
|
||||
@@ -1027,6 +1046,7 @@ pub async fn update_series(
|
||||
total_volumes = EXCLUDED.total_volumes,
|
||||
status = EXCLUDED.status,
|
||||
locked_fields = EXCLUDED.locked_fields,
|
||||
original_name = COALESCE(EXCLUDED.original_name, series_metadata.original_name),
|
||||
updated_at = NOW()
|
||||
"#
|
||||
)
|
||||
@@ -1039,11 +1059,12 @@ pub async fn update_series(
|
||||
.bind(body.total_volumes)
|
||||
.bind(&body.status)
|
||||
.bind(&locked_fields)
|
||||
.bind(&original_name)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
// 3. If renamed, move series_metadata from old name to new name
|
||||
if name != "unclassified" && new_name != name {
|
||||
// 3. If renamed, delete the old series_metadata entry
|
||||
if is_rename {
|
||||
sqlx::query(
|
||||
"DELETE FROM series_metadata WHERE library_id = $1 AND name = $2"
|
||||
)
|
||||
|
||||
@@ -120,6 +120,31 @@ pub async fn scan_library_discovery(
|
||||
.collect();
|
||||
let mut seen_new_series: HashSet<String> = HashSet::new();
|
||||
|
||||
// Load series rename mapping: original filesystem name → current DB name.
|
||||
// This prevents the scanner from recreating old series after a user rename.
|
||||
let rename_rows = sqlx::query(
|
||||
"SELECT original_name, name FROM series_metadata WHERE library_id = $1 AND original_name IS NOT NULL",
|
||||
)
|
||||
.bind(library_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let series_rename_map: HashMap<String, String> = rename_rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let original: String = row.get("original_name");
|
||||
let current: String = row.get("name");
|
||||
(original, current)
|
||||
})
|
||||
.collect();
|
||||
if !series_rename_map.is_empty() {
|
||||
info!(
|
||||
"[SCAN] Loaded {} series rename mapping(s) for library {}",
|
||||
series_rename_map.len(),
|
||||
library_id
|
||||
);
|
||||
}
|
||||
|
||||
let mut seen: HashMap<String, bool> = HashMap::new();
|
||||
let mut library_processed_count = 0i32;
|
||||
let mut last_progress_update = std::time::Instant::now();
|
||||
@@ -324,7 +349,19 @@ pub async fn scan_library_discovery(
|
||||
seen.insert(lookup_path.clone(), true);
|
||||
|
||||
// Fast metadata extraction — no archive I/O
|
||||
let parsed = parse_metadata_fast(&path, format, root);
|
||||
let mut parsed = parse_metadata_fast(&path, format, root);
|
||||
|
||||
// Apply series rename mapping: if the filesystem-derived series name
|
||||
// was renamed by the user, use the current DB name instead.
|
||||
if let Some(ref fs_series) = parsed.series {
|
||||
if let Some(renamed) = series_rename_map.get(fs_series) {
|
||||
debug!(
|
||||
"[SCAN] Mapping renamed series: '{}' → '{}'",
|
||||
fs_series, renamed
|
||||
);
|
||||
parsed.series = Some(renamed.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((file_id, book_id, old_fingerprint)) =
|
||||
existing.get(&lookup_path).cloned()
|
||||
|
||||
Reference in New Issue
Block a user