a / api /src /upgrade /v002_backdrop_position_direction_cache.rs
FTA7700
Update v002_backdrop_position_direction_cache.rs
48bbdc9
Raw History Blame Contribute Delete
7.53 kB
use sea_orm::{ConnectionTrait, DatabaseConnection};
use crate::error::AppError;
/// Migrate backdrop cache keys to include position and direction suffixes.
///
/// Before this change, backdrop cache suffixes were:
/// `{ratings}.s{style}.l{label}.b{badge_size}.z{image_size}`
/// After this change they include position (`.ptr`) and direction (`.dv`):
/// `{ratings}.ptr.s{style}.l{label}.dv.b{badge_size}.z{image_size}`
///
/// All existing backdrops were rendered with the hardcoded defaults
/// (TopRight position, Vertical direction), so we insert those defaults.
pub async fn run(
db: &DatabaseConnection,
cache_dir: &str,
external_cache_only: bool,
) -> Result<(), AppError> {
run_db(db).await?;
run_fs(cache_dir, external_cache_only).await?;
Ok(())
}
/// Database step — insert `.ptr` before badge style and `.dv` after label style
/// in all backdrop cache keys.
pub async fn run_db(db: &impl ConnectionTrait) -> Result<(), AppError> {
let mut total = 0u64;
// Insert `.ptr` (default position = TopRight) before badge style
for style in ["sv", "sh", "sd"] {
let old = format!(".{style}.l");
let new = format!(".ptr.{style}.l");
let result = db
.execute(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
&format!(
"UPDATE image_meta SET cache_key = \
left(cache_key, strpos(cache_key, $1) - 1) || $2 || \
right(cache_key, length(cache_key) - strpos(cache_key, $1) - length($1) + 1) \
WHERE image_type = 'b' AND strpos(cache_key, $1) > 0 AND strpos(cache_key, '.ptr.') = 0"
),
[old.clone().into(), new.into()],
))
.await?;
total += result.rows_affected();
}
// Insert `.dv` (default direction = Vertical) after label style
for label in ["lt", "li", "lo"] {
let old = format!(".{label}.b");
let new = format!(".{label}.dv.b");
let result = db
.execute(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
&format!(
"UPDATE image_meta SET cache_key = \
left(cache_key, strpos(cache_key, $1) - 1) || $2 || \
right(cache_key, length(cache_key) - strpos(cache_key, $1) - length($1) + 1) \
WHERE image_type = 'b' AND strpos(cache_key, $1) > 0 AND strpos(cache_key, '.dv.') = 0"
),
[old.clone().into(), new.into()],
))
.await?;
total += result.rows_affected();
}
tracing::info!(db_rows = total, "backdrop cache keys migrated (added position/direction suffixes)");
Ok(())
}
/// Filesystem step — rename backdrop cache files to include position/direction suffixes.
pub async fn run_fs(cache_dir: &str, external_cache_only: bool) -> Result<(), AppError> {
if external_cache_only {
tracing::info!("backdrop position/direction filesystem rename skipped (external_cache_only)");
return Ok(());
}
let backdrop_dir = std::path::Path::new(cache_dir).join("backdrops");
let renamed = tokio::task::spawn_blocking(move || rename_files(&backdrop_dir))
.await
.map_err(|e| AppError::Other(format!("rename task panicked: {e}")))?
?;
tracing::info!(fs_renamed = renamed, "backdrop cache files renamed (added position/direction suffixes)");
Ok(())
}
/// Recursively rename backdrop cache files to include `.ptr` and `.dv` suffixes.
fn rename_files(dir: &std::path::Path) -> Result<u64, AppError> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(0),
Err(e) => {
return Err(AppError::Other(format!(
"failed to read {}: {e}",
dir.display()
)))
}
};
let mut count = 0u64;
for entry in entries {
let entry = entry.map_err(|e| AppError::Other(e.to_string()))?;
let path = entry.path();
if path.is_dir() {
count += rename_files(&path)?;
} else if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if let Some(new_name) = migrate_name(name) {
let new_path = path.with_file_name(new_name);
std::fs::rename(&path, &new_path).map_err(|e| {
AppError::Other(format!(
"rename failed: {} → {}: {e}",
path.display(),
new_path.display()
))
})?;
count += 1;
}
}
}
Ok(count)
}
/// Transform an old backdrop filename to include default position/direction suffixes.
///
/// Returns `None` if already migrated or no matching pattern found.
fn migrate_name(name: &str) -> Option<String> {
if name.contains(".ptr.") && name.contains(".dv.") {
return None;
}
let mut result = name.to_string();
let mut modified = false;
if !result.contains(".ptr.") {
for style in [".sv.l", ".sh.l", ".sd.l"] {
if let Some(pos) = result.find(style) {
result.insert_str(pos, ".ptr");
modified = true;
break;
}
}
}
if !result.contains(".dv.") {
for (old, new) in [(".lt.b", ".lt.dv.b"), (".li.b", ".li.dv.b"), (".lo.b", ".lo.dv.b")] {
if result.contains(old) {
result = result.replacen(old, new, 1);
modified = true;
break;
}
}
}
if modified { Some(result) } else { None }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn migrate_name_typical() {
assert_eq!(
migrate_name("tt1234567_b_f@mil.sv.lt.bm.zm.jpg"),
Some("tt1234567_b_f@mil.ptr.sv.lt.dv.bm.zm.jpg".to_string()),
);
}
#[test]
fn migrate_name_horizontal_style_icon_label() {
assert_eq!(
migrate_name("tt1234567_b_t@ir.sh.li.bl.zl.jpg"),
Some("tt1234567_b_t@ir.ptr.sh.li.dv.bl.zl.jpg".to_string()),
);
}
#[test]
fn migrate_name_default_style_official_label() {
assert_eq!(
migrate_name("tt1234567_b_f@m.sd.lo.bxs.zs.jpg"),
Some("tt1234567_b_f@m.ptr.sd.lo.dv.bxs.zs.jpg".to_string()),
);
}
#[test]
fn migrate_name_already_migrated() {
assert_eq!(
migrate_name("tt1234567_b_f@mil.ptr.sv.lt.dv.bm.zm.jpg"),
None,
);
}
#[test]
fn migrate_name_no_match() {
assert_eq!(migrate_name("tt1234567.jpg"), None);
}
#[test]
fn migrate_name_extra_small_badge_size() {
assert_eq!(
migrate_name("tt1234567_b_f@mil.sv.lt.bxs.zm.jpg"),
Some("tt1234567_b_f@mil.ptr.sv.lt.dv.bxs.zm.jpg".to_string()),
);
}
#[test]
fn migrate_name_extra_large_badge_size() {
assert_eq!(
migrate_name("tt1234567_b_f@mil.sv.lt.bxl.zvl.jpg"),
Some("tt1234567_b_f@mil.ptr.sv.lt.dv.bxl.zvl.jpg".to_string()),
);
}
#[test]
fn migrate_name_zero_ratings() {
assert_eq!(
migrate_name("tt1234567_b_f@.sv.lt.bm.zm.jpg"),
Some("tt1234567_b_f@.ptr.sv.lt.dv.bm.zm.jpg".to_string()),
);
}
}