Rewrite Curator in Rust with an egui GUI
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
//! Readers for Curator's *pre-index* metadata layouts.
|
||||
//!
|
||||
//! Before the unified pool index, Tagger / early Curator stored a movie's
|
||||
//! metadata in a per-file sidecar `.{filename}.!tag` (JSON) next to the video.
|
||||
//! When importing a file from such a pool we can recover its ČSFD link (and
|
||||
//! title / year) from that sidecar — or from an old `.Curator.!index` found by
|
||||
//! walking up from the file. Read-only recovery for the import dialog.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::paths;
|
||||
use super::pool_index::INDEX_FILENAME;
|
||||
|
||||
pub const SIDECAR_SUFFIX: &str = ".!tag";
|
||||
const MAX_INDEX_WALK_UP: usize = 4;
|
||||
|
||||
/// What an old sidecar / index record can still tell us about a movie.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct LegacyMeta {
|
||||
pub csfd_url: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub year: Option<String>,
|
||||
/// "sidecar" | "index".
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
fn nonempty_str(v: Option<&Value>) -> Option<String> {
|
||||
v.and_then(Value::as_str)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from)
|
||||
}
|
||||
|
||||
/// Pull the useful fields out of a stored movie record (any schema version).
|
||||
fn record_to_meta(record: &Value, source: &str) -> Option<LegacyMeta> {
|
||||
let rec = record.as_object()?;
|
||||
let csfd = nonempty_str(rec.get("csfd_url")).or_else(|| nonempty_str(rec.get("csfd_link")));
|
||||
let title = nonempty_str(rec.get("title"));
|
||||
let from = |key: &str| {
|
||||
rec.get(key)
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|o| o.get("year"))
|
||||
.cloned()
|
||||
};
|
||||
let raw_year = from("meta")
|
||||
.filter(super::pool_index::truthy)
|
||||
.or_else(|| from("csfd_cache"))
|
||||
.filter(|v| !v.is_null());
|
||||
let year = raw_year
|
||||
.map(|v| {
|
||||
v.as_str()
|
||||
.map_or_else(|| v.to_string(), String::from)
|
||||
.trim()
|
||||
.to_string()
|
||||
})
|
||||
.filter(|y| !y.is_empty() && y.chars().all(|c| c.is_ascii_digit()));
|
||||
if csfd.is_none() && title.is_none() && year.is_none() {
|
||||
return None;
|
||||
}
|
||||
Some(LegacyMeta {
|
||||
csfd_url: csfd,
|
||||
title,
|
||||
year,
|
||||
source: source.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_json(path: &Path) -> Option<Value> {
|
||||
serde_json::from_str(&fs::read_to_string(path).ok()?).ok()
|
||||
}
|
||||
|
||||
fn from_sidecar(video_path: &Path) -> Option<LegacyMeta> {
|
||||
let sidecar = video_path
|
||||
.parent()?
|
||||
.join(format!(".{}{SIDECAR_SUFFIX}", paths::name(video_path)));
|
||||
if !sidecar.is_file() {
|
||||
return None;
|
||||
}
|
||||
record_to_meta(&read_json(&sidecar)?, "sidecar")
|
||||
}
|
||||
|
||||
/// The record for `video_path` in an index's `movies` map — exact pool-relative
|
||||
/// key first, then a unique basename match.
|
||||
fn lookup_in_index<'a>(
|
||||
movies: &'a serde_json::Map<String, Value>,
|
||||
video_path: &Path,
|
||||
index_dir: &Path,
|
||||
) -> Option<&'a Value> {
|
||||
if video_path.starts_with(index_dir)
|
||||
&& let Some(rec) = movies.get(&paths::posix_relative(video_path, index_dir))
|
||||
{
|
||||
return Some(rec);
|
||||
}
|
||||
let name = paths::name(video_path);
|
||||
let matches: Vec<&Value> = movies
|
||||
.iter()
|
||||
.filter(|(k, _)| k.rsplit('/').next() == Some(name.as_str()))
|
||||
.map(|(_, v)| v)
|
||||
.collect();
|
||||
(matches.len() == 1).then(|| matches[0])
|
||||
}
|
||||
|
||||
fn from_legacy_index(video_path: &Path) -> Option<LegacyMeta> {
|
||||
let mut directory = video_path.parent()?.to_path_buf();
|
||||
for _ in 0..=MAX_INDEX_WALK_UP {
|
||||
let index_path = directory.join(INDEX_FILENAME);
|
||||
if index_path.is_file() {
|
||||
let data = read_json(&index_path)?;
|
||||
let obj = data.as_object()?;
|
||||
let movies = match obj.get("movies") {
|
||||
Some(m) => m.as_object()?,
|
||||
None => obj,
|
||||
};
|
||||
return record_to_meta(lookup_in_index(movies, video_path, &directory)?, "index");
|
||||
}
|
||||
match directory.parent() {
|
||||
Some(parent) if parent != directory => directory = parent.to_path_buf(),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Recover ČSFD link / title / year from a pre-index layout: a `.{name}.!tag`
|
||||
/// sidecar next to the video, then a `.Curator.!index` up to 4 levels up.
|
||||
pub fn read_legacy_meta(video_path: &Path) -> Option<LegacyMeta> {
|
||||
from_sidecar(video_path).or_else(|| from_legacy_index(video_path))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn video(dir: &Path, rel: &str) -> std::path::PathBuf {
|
||||
let p = dir.join(rel);
|
||||
fs::create_dir_all(p.parent().unwrap()).unwrap();
|
||||
fs::write(&p, "x").unwrap();
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reads_tagger_era_sidecar() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let v = video(dir.path(), "Matrix.mkv");
|
||||
fs::write(dir.path().join(".Matrix.mkv.!tag"),
|
||||
json!({"csfd_link": "https://www.csfd.cz/film/9499/", "title": "Matrix", "csfd_cache": {"year": 1999}}).to_string()).unwrap();
|
||||
let meta = read_legacy_meta(&v).unwrap();
|
||||
assert_eq!(
|
||||
meta.csfd_url.as_deref(),
|
||||
Some("https://www.csfd.cz/film/9499/")
|
||||
);
|
||||
assert_eq!(meta.year.as_deref(), Some("1999"));
|
||||
assert_eq!(meta.source, "sidecar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reads_old_index_by_key_and_by_basename() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let v = video(dir.path(), "Filmy/sub/Matrix.mkv");
|
||||
fs::write(dir.path().join(INDEX_FILENAME),
|
||||
json!({"movies": {"Filmy/sub/Matrix.mkv": {"csfd_url": "u1"}, "Old/Other.mkv": {"title": "Other"}}}).to_string()).unwrap();
|
||||
assert_eq!(
|
||||
read_legacy_meta(&v).unwrap().csfd_url.as_deref(),
|
||||
Some("u1")
|
||||
);
|
||||
let moved = video(dir.path(), "Elsewhere/Other.mkv");
|
||||
assert_eq!(
|
||||
read_legacy_meta(&moved).unwrap().title.as_deref(),
|
||||
Some("Other")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sidecar_wins_over_index() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let v = video(dir.path(), "Matrix.mkv");
|
||||
fs::write(
|
||||
dir.path().join(".Matrix.mkv.!tag"),
|
||||
json!({"csfd_url": "side"}).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.path().join(INDEX_FILENAME),
|
||||
json!({"movies": {"Matrix.mkv": {"csfd_url": "idx"}}}).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
read_legacy_meta(&v).unwrap().csfd_url.as_deref(),
|
||||
Some("side")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_none_cases() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let v = video(dir.path(), "Matrix.mkv");
|
||||
assert!(read_legacy_meta(&v).is_none());
|
||||
fs::write(dir.path().join(".Matrix.mkv.!tag"), "{broken").unwrap();
|
||||
assert!(read_legacy_meta(&v).is_none());
|
||||
fs::write(
|
||||
dir.path().join(".Matrix.mkv.!tag"),
|
||||
json!({"tags": []}).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(read_legacy_meta(&v).is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user