use crate::{changelog::TableDiff, output::OutputFolderKey, time::UnixEpochTs, url::ResolvedUrl};
use anyhow::{Context as _, Result, ensure};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Table(pub Data, pub Context);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Data {
pub web_url: ResolvedUrl,
pub name: String,
pub symbol: String,
pub entries: Vec<Entry>,
pub folder_order: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Context {
pub last_update: Option<UnixEpochTs>,
pub playlist_id: Option<crate::db::TableId>,
pub user_name: Option<String>,
pub user_symbol: Option<String>,
pub output: OutputFolderKey,
pub summary_changelog: String,
pub full_changelog: String,
pub entry_diff_to_save_to_db: Vec<TableDiff>,
pub pending_removal: bool,
pub status: Status,
pub edited_name: Option<String>,
pub edited_symbol: Option<String>,
pub edited_url: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Entry {
pub md5: String,
pub level: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Status {
Ready,
Updating,
Error(String),
}
impl Table {
pub fn apply_update(
accessors: &[&dyn crate::changelog::SongNameAccessor],
tables: &mut Vec<Self>,
data: Data,
locator: &crate::fetch::TableLocator,
now: UnixEpochTs,
) -> Result<()> {
let old = locator
.locate(tables)
.with_context(|| format!("failed to find table {locator}"))?;
let old = tables.remove(old);
let new_diff = TableDiff::new(&old.0.entries, &data.entries, &data.folder_order);
let summary_changelog = new_diff.format_summary()?;
let full_changelog =
new_diff.format_full(accessors, &data.symbol, old.1.user_symbol.as_deref())?;
if !summary_changelog.is_empty() {
log::info!(
"table {} updated: {}\n{}",
data.name,
summary_changelog,
full_changelog
);
log::debug!(
"changed={} deleted={} new={}",
new_diff.changed.len(),
new_diff.deleted.len(),
new_diff.new.len(),
);
}
let mut diff = old.1.entry_diff_to_save_to_db;
diff.push(new_diff);
let new = Self(
data,
Context {
status: Status::Ready,
summary_changelog,
full_changelog,
entry_diff_to_save_to_db: diff,
last_update: Some(now),
..old.1
},
);
let idx = tables.partition_point(|t| t.0.name < new.0.name);
tables.insert(idx, new);
Ok(())
}
pub fn commit_removals(tables: &mut Vec<Self>) {
let old_len = tables.len();
tables.retain(|t| !t.1.pending_removal);
if tables.len() != old_len {
log::info!("Removed {} table(s) while saving", old_len - tables.len());
}
}
pub fn insert_new(
tables: &mut Vec<Self>,
web_url: ResolvedUrl,
output: OutputFolderKey,
) -> Result<()> {
ensure!(
!tables.iter().any(|t| t.0.web_url == web_url),
"Table {} already exists",
web_url.as_str()
);
let table = Self(
Data {
name: {
let mut name = web_url.as_str().to_string();
let forbidden_in_fs = ['\n', '\r', '\'', '/', '\\'];
for c in forbidden_in_fs {
while let Some(i) = name.find(c) {
name.replace_range(i..=i, "-");
}
}
name
},
web_url,
symbol: "?".to_string(),
entries: vec![],
folder_order: vec![],
},
Context {
summary_changelog: String::new(),
full_changelog: String::new(),
entry_diff_to_save_to_db: vec![],
edited_name: None,
edited_symbol: None,
edited_url: None,
last_update: None,
pending_removal: false,
playlist_id: None,
status: Status::Ready,
user_name: None,
user_symbol: None,
output,
},
);
tables.insert(tables.partition_point(|t| t.0.name < table.0.name), table);
Ok(())
}
#[must_use]
pub fn wants_updating(&self, now: UnixEpochTs, update_interval: UnixEpochTs) -> bool {
pub const NEGATIVE_NEVER: UnixEpochTs = UnixEpochTs(0);
if self.1.pending_removal || self.1.status == Status::Updating {
return false;
}
if self.0.web_url.as_str().starts_with("file://") {
return true;
}
self.1.last_update.unwrap_or(NEGATIVE_NEVER).0 + update_interval.0 < now.0
}
}
#[cfg(test)]
impl Table {
#[must_use]
pub fn empty() -> Self {
Self(
Data {
web_url: "http://".try_into().unwrap(),
name: String::new(),
symbol: String::new(),
entries: vec![],
folder_order: vec![],
},
Context {
summary_changelog: String::new(),
full_changelog: String::new(),
entry_diff_to_save_to_db: vec![],
edited_name: None,
edited_symbol: None,
edited_url: None,
last_update: None,
pending_removal: false,
playlist_id: None,
user_name: None,
user_symbol: None,
output: OutputFolderKey(String::new()),
status: Status::Ready,
},
)
}
#[must_use]
pub fn with_entry(mut self, md5: usize, level: impl Into<String>) -> Self {
self.0.entries.push(Entry {
md5: format!("foodfoodfoodfoodfoodfoodfood{md5:0>4x}"),
level: level.into(),
});
self
}
#[must_use]
pub fn with_id(mut self, id: crate::db::TableId) -> Self {
self.1.playlist_id = Some(id);
self
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.0.name = name.into();
self
}
#[must_use]
pub fn with_output(mut self, output: impl Into<String>) -> Self {
self.1.output = OutputFolderKey(output.into());
self
}
#[must_use]
pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
self.0.symbol = symbol.into();
self
}
#[must_use]
pub fn with_updated_changelog(mut self, old_entries: &[Entry]) -> Self {
self.1
.entry_diff_to_save_to_db
.push(TableDiff::new(old_entries, &self.0.entries, &[]));
self
}
#[must_use]
pub fn with_url(mut self, web_url: impl Into<String>) -> Self {
self.0.web_url = web_url.into().try_into().unwrap();
self
}
#[must_use]
pub fn with_user_name(mut self, name: impl Into<String>) -> Self {
self.1.user_name = Some(name.into());
self
}
#[must_use]
pub fn with_user_symbol(mut self, symbol: impl Into<String>) -> Self {
self.1.user_symbol = Some(symbol.into());
self
}
}
#[cfg(test)]
mod tests {
#[test]
fn apply_update_with_remaining_db_diff() {
use super::Table;
use crate::{changelog::TableDiff, fetch::TableLocator, time::UnixEpochTs};
let now = UnixEpochTs(0);
let mut tables = vec![
Table::empty()
.with_entry(0, "from")
.with_entry(1, "deleted"),
];
{
let new_data = Table::empty().with_entry(0, "to").with_entry(2, "new").0;
let locator = TableLocator::new_for_table(&tables[0]);
Table::apply_update(&[], &mut tables, new_data, &locator, now).unwrap();
}
assert_eq!(
tables[0].1.entry_diff_to_save_to_db,
[TableDiff {
changed: vec![(
"foodfoodfoodfoodfoodfoodfood0000".to_string(),
"from".to_string(),
"to".to_string(),
)],
deleted: vec![(
"foodfoodfoodfoodfoodfoodfood0001".to_string(),
"deleted".to_string(),
)],
new: vec![(
"foodfoodfoodfoodfoodfoodfood0002".to_string(),
"new".to_string(),
)],
}]
);
{
let new_data = tables[0].clone().with_entry(3, "another-new").0;
let locator = TableLocator::new_for_table(&tables[0]);
Table::apply_update(&[], &mut tables, new_data, &locator, now).unwrap();
}
{
assert_eq!(
tables[0].1.entry_diff_to_save_to_db,
[
TableDiff {
changed: vec![(
"foodfoodfoodfoodfoodfoodfood0000".to_string(),
"from".to_string(),
"to".to_string(),
)],
deleted: vec![(
"foodfoodfoodfoodfoodfoodfood0001".to_string(),
"deleted".to_string(),
)],
new: vec![(
"foodfoodfoodfoodfoodfoodfood0002".to_string(),
"new".to_string(),
)],
},
TableDiff {
changed: vec![],
deleted: vec![],
new: vec![(
"foodfoodfoodfoodfoodfoodfood0003".to_string(),
"another-new".to_string(),
),],
}
]
);
}
}
}