aede_core/analysis/
export.rs1use crate::json::Json;
3use crate::model::{Catalog, EntityKind, Id};
4use std::collections::BTreeSet;
5
6const MAX_REPORT_BYTES: usize = 16 * 1024 * 1024;
7
8#[derive(Debug, PartialEq, Eq)]
10pub enum ExportError {
11 InvalidSelection,
13 MissingAnalysis,
15 StaleAnalysis,
17 IncompleteAnalysis,
19 UnsupportedVersion,
21 TooLarge,
23}
24pub fn report(catalog: &Catalog, kind: EntityKind, id: Id) -> Result<String, ExportError> {
26 let (root, tracks) = match kind {
27 EntityKind::Track => {
28 let track = catalog.track(id).ok_or(ExportError::InvalidSelection)?;
29 let file = catalog
30 .file(track.file_id)
31 .ok_or(ExportError::InvalidSelection)?;
32 let root = std::path::Path::new(&file.path)
33 .parent()
34 .ok_or(ExportError::InvalidSelection)?;
35 (root.to_string_lossy().into_owned(), vec![id])
36 }
37 EntityKind::Release => {
38 let release = catalog.release(id).ok_or(ExportError::InvalidSelection)?;
39 (release.folder.clone(), release.track_ids.clone())
40 }
41 _ => return Err(ExportError::InvalidSelection),
42 };
43 if tracks.is_empty() {
44 return Err(ExportError::MissingAnalysis);
45 }
46 if tracks.len() > 2048 {
47 return Err(ExportError::TooLarge);
48 }
49 let mut entries = Vec::new();
50 let mut seen = BTreeSet::new();
51 let mut version = 0;
52 let mut total_bytes = 0;
53 for id in tracks {
54 let track = catalog.track(id).ok_or(ExportError::InvalidSelection)?;
55 let file = catalog
56 .file(track.file_id)
57 .ok_or(ExportError::InvalidSelection)?;
58 if !seen.insert(&file.path) {
59 continue;
60 }
61 let records: Vec<_> = catalog
62 .analyses
63 .iter()
64 .filter(|record| record.source == "flaccompagnon" && record.path == file.path)
65 .collect();
66 if records.is_empty() {
67 return Err(ExportError::MissingAnalysis);
68 }
69 let record = records
70 .into_iter()
71 .filter(|record| record.still_applies(file.size, file.mtime))
72 .max_by_key(|record| (record.result_at_ns, record.imported_at))
73 .ok_or(ExportError::StaleAnalysis)?;
74 if record.source_version > super::SUPPORTED_VERSION {
75 return Err(ExportError::UnsupportedVersion);
76 }
77 let entry = record
78 .source_data
79 .as_ref()
80 .filter(|entry| {
81 matches!(entry, Json::Obj(_)) && entry.get("path").and_then(Json::as_str).is_some()
82 })
83 .ok_or(ExportError::IncompleteAnalysis)?;
84 total_bytes += entry
87 .to_string_pretty_bounded(MAX_REPORT_BYTES - total_bytes)
88 .map_err(|_| ExportError::TooLarge)?
89 .len();
90 let mut entry = entry.clone();
94 entry.set(
95 super::RESULT_DATE_FIELD,
96 record.result_at_ns.to_string().into(),
97 );
98 version = version.max(record.source_version);
99 entries.push(entry);
100 }
101 let has_flac = entries.iter().any(|entry| {
102 entry
103 .get("flac_md5")
104 .is_some_and(|value| !matches!(value, Json::Null))
105 });
106 let mut inner = Json::obj();
107 inner.set("root", Json::Str(root));
108 inner.set("files", Json::Arr(entries));
109 inner.set("has_flac", Json::Bool(has_flac));
110 let mut outer = Json::obj();
111 outer.set("format", Json::Str(super::FLACCOMPAGNON_FORMAT.into()));
112 outer.set("version", Json::Num(f64::from(version)));
113 outer.set("report", inner);
114 outer
115 .to_string_pretty_bounded(MAX_REPORT_BYTES)
116 .map_err(|_| ExportError::TooLarge)
117}
118#[cfg(test)]
119#[path = "export_tests.rs"]
120mod tests;