Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9e16dd807 | |||
| b958d4521e |
Generated
+1
-1
@@ -1141,7 +1141,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "furumusic"
|
name = "furumusic"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "furumusic"
|
name = "furumusic"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL"
|
description = "Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL"
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ fn extract_tags(tags: &[symphonia::core::meta::Tag], meta: &mut RawMetadata) {
|
|||||||
}
|
}
|
||||||
StandardTagKey::Date | StandardTagKey::OriginalDate => {
|
StandardTagKey::Date | StandardTagKey::OriginalDate => {
|
||||||
if meta.year.is_none() {
|
if meta.year.is_none() {
|
||||||
meta.year = value[..4.min(value.len())].parse().ok();
|
let year_prefix: String = value.chars().take(4).collect();
|
||||||
|
meta.year = year_prefix.parse().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StandardTagKey::Genre => {
|
StandardTagKey::Genre => {
|
||||||
|
|||||||
+1
-1
@@ -85,7 +85,7 @@ pub async fn probe_llm(
|
|||||||
let body_text = resp.text().await.unwrap_or_default();
|
let body_text = resp.text().await.unwrap_or_default();
|
||||||
return AgentProbeResult {
|
return AgentProbeResult {
|
||||||
latency_ms,
|
latency_ms,
|
||||||
error: format!("HTTP {status}: {}", &body_text[..body_text.len().min(300)]),
|
error: format!("HTTP {status}: {}", body_text.chars().take(300).collect::<String>()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ async fn call_llm_chat(
|
|||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
let body = resp.text().await.unwrap_or_default();
|
let body = resp.text().await.unwrap_or_default();
|
||||||
tracing::error!(%status, body = &body[..body.len().min(500)], "LLM API error");
|
let body_preview: String = body.chars().take(500).collect();
|
||||||
|
tracing::error!(%status, body = %body_preview, "LLM API error");
|
||||||
anyhow::bail!("LLM returned {}: {}", status, body);
|
anyhow::bail!("LLM returned {}: {}", status, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +441,7 @@ fn parse_batch_response(
|
|||||||
anyhow::anyhow!(
|
anyhow::anyhow!(
|
||||||
"Failed to parse batch LLM response: {} — raw: {}",
|
"Failed to parse batch LLM response: {} — raw: {}",
|
||||||
e,
|
e,
|
||||||
&response[..response.len().min(500)]
|
response.chars().take(500).collect::<String>()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ async fn process_folder_batch(
|
|||||||
format!("batch({})", file_count)
|
format!("batch({})", file_count)
|
||||||
} else {
|
} else {
|
||||||
let short = truncate_path(folder_rel, 20);
|
let short = truncate_path(folder_rel, 20);
|
||||||
format!("{short}({})", file_count)
|
truncate_utf8_bytes(&format!("{short}({})", file_count), 32)
|
||||||
};
|
};
|
||||||
let mut run = match JobRun::create_running(db, "file_process", &trigger_label).await {
|
let mut run = match JobRun::create_running(db, "file_process", &trigger_label).await {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -949,9 +949,69 @@ fn sanitize_filename(name: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn truncate_path(path: &str, max_len: usize) -> String {
|
fn truncate_path(path: &str, max_len: usize) -> String {
|
||||||
if path.len() <= max_len {
|
let char_count = path.chars().count();
|
||||||
|
if char_count <= max_len {
|
||||||
path.to_owned()
|
path.to_owned()
|
||||||
|
} else if max_len <= 3 {
|
||||||
|
".".repeat(max_len)
|
||||||
} else {
|
} else {
|
||||||
format!("...{}", &path[path.len() - (max_len - 3)..])
|
let suffix: String = path
|
||||||
|
.chars()
|
||||||
|
.skip(char_count - (max_len - 3))
|
||||||
|
.collect();
|
||||||
|
format!("...{suffix}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_utf8_bytes(value: &str, max_bytes: usize) -> String {
|
||||||
|
if value.len() <= max_bytes {
|
||||||
|
return value.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
if max_bytes <= 3 {
|
||||||
|
return ".".repeat(max_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
let suffix_budget = max_bytes - 3;
|
||||||
|
let mut suffix = Vec::new();
|
||||||
|
let mut suffix_len = 0;
|
||||||
|
for ch in value.chars().rev() {
|
||||||
|
let ch_len = ch.len_utf8();
|
||||||
|
if suffix_len + ch_len > suffix_budget {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
suffix.push(ch);
|
||||||
|
suffix_len += ch_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = String::from("...");
|
||||||
|
for ch in suffix.iter().rev() {
|
||||||
|
result.push(*ch);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{truncate_path, truncate_utf8_bytes};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_path_handles_unicode_boundaries() {
|
||||||
|
assert_eq!(
|
||||||
|
truncate_path("KUNTEYNIR/Блёвбургер", 20),
|
||||||
|
"KUNTEYNIR/Блёвбургер"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
truncate_path("KUNTEYNIR/ОченьДлинноеНазвание", 12),
|
||||||
|
"...еНазвание"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_utf8_bytes_handles_limited_string_boundaries() {
|
||||||
|
let value = truncate_utf8_bytes("KUNTEYNIR/Блёвбургер(1)", 32);
|
||||||
|
assert!(value.len() <= 32);
|
||||||
|
assert!(value.is_char_boundary(value.len()));
|
||||||
|
assert!(value.ends_with("Блёвбургер(1)"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-6
@@ -30,6 +30,41 @@ fn now_iso() -> LimitedString<32> {
|
|||||||
LimitedString::new(&chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap()
|
LimitedString::new(&chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn truncate_utf8_bytes(value: &str, max_bytes: usize) -> String {
|
||||||
|
if value.len() <= max_bytes {
|
||||||
|
return value.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
if max_bytes <= 3 {
|
||||||
|
return ".".repeat(max_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
let suffix_budget = max_bytes - 3;
|
||||||
|
let mut suffix = Vec::new();
|
||||||
|
let mut suffix_len = 0;
|
||||||
|
for ch in value.chars().rev() {
|
||||||
|
let ch_len = ch.len_utf8();
|
||||||
|
if suffix_len + ch_len > suffix_budget {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
suffix.push(ch);
|
||||||
|
suffix_len += ch_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = String::from("...");
|
||||||
|
for ch in suffix.iter().rev() {
|
||||||
|
result.push(*ch);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn limited_string<const N: u32>(value: &str) -> LimitedString<N> {
|
||||||
|
LimitedString::new(value).unwrap_or_else(|_| {
|
||||||
|
let truncated = truncate_utf8_bytes(value, N as usize);
|
||||||
|
LimitedString::new(&truncated).unwrap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl ScheduledJob {
|
impl ScheduledJob {
|
||||||
pub async fn list_all(db: &Database) -> cot::db::Result<Vec<Self>> {
|
pub async fn list_all(db: &Database) -> cot::db::Result<Vec<Self>> {
|
||||||
Self::objects().all(db).await
|
Self::objects().all(db).await
|
||||||
@@ -138,14 +173,14 @@ impl JobRun {
|
|||||||
pub async fn create_running(db: &Database, job_name: &str, trigger: &str) -> cot::db::Result<Self> {
|
pub async fn create_running(db: &Database, job_name: &str, trigger: &str) -> cot::db::Result<Self> {
|
||||||
let mut run = Self {
|
let mut run = Self {
|
||||||
id: Auto::auto(),
|
id: Auto::auto(),
|
||||||
job_name: LimitedString::new(job_name).unwrap(),
|
job_name: limited_string(job_name),
|
||||||
status: LimitedString::new("running").unwrap(),
|
status: LimitedString::new("running").unwrap(),
|
||||||
started_at: now_iso(),
|
started_at: now_iso(),
|
||||||
finished_at: None,
|
finished_at: None,
|
||||||
duration_ms: None,
|
duration_ms: None,
|
||||||
log_output: None,
|
log_output: None,
|
||||||
error_message: None,
|
error_message: None,
|
||||||
trigger: LimitedString::new(trigger).unwrap(),
|
trigger: limited_string(trigger),
|
||||||
};
|
};
|
||||||
run.insert(db).await?;
|
run.insert(db).await?;
|
||||||
Ok(run)
|
Ok(run)
|
||||||
@@ -273,14 +308,14 @@ impl JobRunRow {
|
|||||||
fn into_model(self) -> JobRun {
|
fn into_model(self) -> JobRun {
|
||||||
JobRun {
|
JobRun {
|
||||||
id: Auto::Fixed(self.id),
|
id: Auto::Fixed(self.id),
|
||||||
job_name: LimitedString::new(&self.job_name).unwrap(),
|
job_name: limited_string(&self.job_name),
|
||||||
status: LimitedString::new(&self.status).unwrap(),
|
status: limited_string(&self.status),
|
||||||
started_at: LimitedString::new(&self.started_at).unwrap(),
|
started_at: limited_string(&self.started_at),
|
||||||
finished_at: self.finished_at,
|
finished_at: self.finished_at,
|
||||||
duration_ms: self.duration_ms,
|
duration_ms: self.duration_ms,
|
||||||
log_output: self.log_output,
|
log_output: self.log_output,
|
||||||
error_message: self.error_message,
|
error_message: self.error_message,
|
||||||
trigger: LimitedString::new(&self.trigger).unwrap(),
|
trigger: limited_string(&self.trigger),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user