Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 143 additions & 97 deletions Cargo.lock

Large diffs are not rendered by default.

20 changes: 19 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
[workspace]
resolver = "3"
members = ["server", "mlm_db", "mlm_parse", "mlm_mam", "mlm_meta"]
members = [
"server",
"mlm_db",
"mlm_parse",
"mlm_mam",
"mlm_meta",
"mlm_core",
"mlm_web_askama",
]

# Faster dev builds for WASM
[profile.dev.package."*"]
opt-level = 1

# Keep debug builds fast for the server
[profile.dev]
opt-level = 0
codegen-units = 256
debug = 1
61 changes: 61 additions & 0 deletions mlm_core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[package]
name = "mlm_core"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.100"
axum = { version = "0.8.4", features = ["query", "macros"] }
bytes = "1.11.0"
dirs = "6.0"
figment = { version = "0.10", features = ["toml", "env"] }
file-id = "0.2.2"
futures = "0.3"
htmlentity = "1.3.2"
itertools = "0.14.0"
lava_torrent = { git = "https://github.com/StirlingMouse/lava_torrent.git" }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Pin git dependencies to specific commits for reproducible builds.

Multiple git dependencies (lava_torrent, native_db, qbit, sanitize-filename) reference branches without commit hashes. This can cause builds to break or behave differently when upstream branches are updated.

♻️ Suggested approach
-lava_torrent = { git = "https://github.com/StirlingMouse/lava_torrent.git" }
+lava_torrent = { git = "https://github.com/StirlingMouse/lava_torrent.git", rev = "<commit-hash>" }

Apply similar changes to native_db, qbit, and sanitize-filename.

Also applies to: 23-23, 27-27, 32-32

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlm_core/Cargo.toml` at line 16, The git dependencies in Cargo.toml (e.g.,
the lava_torrent entry) should be pinned to specific commit SHAs to ensure
reproducible builds; update each git dependency line (lava_torrent, native_db,
qbit, sanitize-filename) to include the rev = "<commit-sha>" (or replace branch
URL with the exact commit) instead of relying on a branch head so the dependency
fetch is deterministic and stable.

log = "0.4.27"
matchr = "0.2.5"
mlm_db = { path = "../mlm_db" }
mlm_mam = { path = "../mlm_mam" }
mlm_parse = { path = "../mlm_parse" }
mlm_meta = { path = "../mlm_meta" }
native_db = { git = "https://github.com/StirlingMouse/native_db.git", branch = "0.8.x" }
native_model = "0.4.20"
once_cell = "1.21.3"
openssl = { version = "0.10.73", features = ["vendored"] }
qbit = { git = "https://github.com/StirlingMouse/qbittorrent-webui-api.git" }
quick-xml = { version = "0.38.0", features = ["serialize"] }
regex = "1.12.2"
reqwest = { version = "0.12.20", default-features = false, features = ["json", "rustls-tls"] }
reqwest_cookie_store = "0.8.0"
sanitize-filename = { git = "https://github.com/StirlingMouse/sanitize-filename.git" }
scraper = "0.23.1"
serde = "1.0.136"
serde_derive = "1.0.136"
serde_json = "1.0.140"
serde-nested-json = "0.1.3"
sublime_fuzzy = "0.7.0"
thiserror = "2.0.17"
time = { version = "0.3.41", features = [
"formatting",
"local-offset",
"macros",
"serde",
] }
tokio = { version = "1.45.1", features = ["fs", "macros", "rt-multi-thread", "sync", "time"] }
tokio-stream = { version = "0.1.17", features = ["sync"] }
tokio-util = "0.7"
toml = "0.8.23"
tower = { version = "0.5.2" }
tracing = "0.1"
tracing-appender = { version = "0.2.3" }
tracing-subscriber = { version = "0.3", features = [
"local-time",
"env-filter",
"tracing-log",
] }
tracing-panic = { version = "0.1.2" }
unidecode = "0.3.0"
urlencoding = "2.1.3"
uuid = { version = "1.17.0", features = ["serde", "v4", "js"] }
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ pub fn create_metadata(meta: &TorrentMeta) -> serde_json::Value {
"series": &meta.series.iter().map(format_serie).collect::<Vec<_>>(),
"title": title,
"subtitle": subtitle,
"description": meta.description,
"description": clean_html(&meta.description),
"isbn": meta.ids.get(ids::ISBN),
"asin": meta.ids.get(ids::ASIN),
"tags": if flags.lgbt == Some(true) { Some(vec!["LGBT"]) } else { None },
Expand Down
128 changes: 105 additions & 23 deletions server/src/autograbber.rs → mlm_core/src/autograbber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub async fn run_autograbber(
autograb_trigger: Sender<()>,
index: usize,
autograb_config: Arc<TorrentSearch>,
events: &crate::stats::Events,
) -> Result<()> {
// Make sure we are only running one autograbber at a time
let _guard = AUTOGRABBER_MUTEX.lock().await;
Expand Down Expand Up @@ -96,6 +97,7 @@ pub async fn run_autograbber(
},
&mam,
max_torrents,
events,
)
.await
.context("search_torrents")?;
Expand All @@ -116,14 +118,15 @@ pub async fn search_and_select_torrents(
fields: SearchFields,
mam: &MaM<'_>,
max_torrents: u64,
events: &crate::stats::Events,
) -> Result<u64> {
let torrents = search_torrents(torrent_search, fields, mam)
.await
.context("search_torrents")?;

if torrent_search.mark_removed {
let torrents = torrents.collect::<Vec<_>>();
mark_removed_torrents(db, mam, &torrents)
mark_removed_torrents(db, mam, &torrents, events)
.await
.context("mark_removed_torrents")?;

Expand All @@ -140,6 +143,7 @@ pub async fn search_and_select_torrents(
torrent_search.dry_run,
max_torrents,
None,
events,
)
.await
.context("select_torrents");
Expand All @@ -158,6 +162,7 @@ pub async fn search_and_select_torrents(
torrent_search.dry_run,
max_torrents,
None,
events,
)
.await
.context("select_torrents")
Expand Down Expand Up @@ -305,6 +310,7 @@ pub async fn mark_removed_torrents(
db: &Database<'_>,
mam: &MaM<'_>,
torrents: &[MaMTorrent],
events: &crate::stats::Events,
) -> Result<()> {
if let (Some(first), Some(last)) = (torrents.first(), torrents.last()) {
let ids = first.id.min(last.id)..=first.id.max(last.id);
Expand All @@ -324,8 +330,12 @@ pub async fn mark_removed_torrents(
rw.upsert(torrent)?;
rw.commit()?;
drop(guard);
write_event(db, Event::new(tid, Some(id), EventType::RemovedFromTracker))
.await;
write_event(
db,
events,
Event::new(tid, Some(id), EventType::RemovedFromTracker),
)
.await;
}
sleep(Duration::from_millis(400)).await;
}
Expand All @@ -350,6 +360,7 @@ pub async fn select_torrents<T: Iterator<Item = MaMTorrent>>(
dry_run: bool,
max_torrents: u64,
goodreads_id: Option<u64>,
events: &crate::stats::Events,
) -> Result<u64> {
let mut selected_torrents = 0;
'torrent: for torrent in torrents {
Expand Down Expand Up @@ -390,13 +401,15 @@ pub async fn select_torrents<T: Iterator<Item = MaMTorrent>>(
let mut updated = old_selected.clone();
updated.unsat_buffer = Some(unsat_buffer);
if updated.meta != meta {
update_selected_torrent_meta(db, rw_opt.unwrap(), mam, updated, meta).await?;
update_selected_torrent_meta(db, rw_opt.unwrap(), mam, updated, meta, events)
.await?;
} else {
rw.update(old_selected, updated)?;
rw_opt.unwrap().1.commit()?;
}
} else if old_selected.meta != meta {
update_selected_torrent_meta(db, rw_opt.unwrap(), mam, old_selected, meta).await?;
update_selected_torrent_meta(db, rw_opt.unwrap(), mam, old_selected, meta, events)
.await?;
}
trace!("Torrent {} is already selected", torrent.id);
continue;
Expand All @@ -420,6 +433,7 @@ pub async fn select_torrents<T: Iterator<Item = MaMTorrent>>(
meta,
false,
cost == Cost::MetadataOnlyAdd,
events,
)
.await?;
}
Expand Down Expand Up @@ -467,7 +481,8 @@ pub async fn select_torrents<T: Iterator<Item = MaMTorrent>>(
for old in old_selected {
if old.mam_id == torrent.id {
if old.meta != meta {
update_selected_torrent_meta(db, rw_opt.unwrap(), mam, old, meta).await?;
update_selected_torrent_meta(db, rw_opt.unwrap(), mam, old, meta, events)
.await?;
}
trace!("Torrent {} is already selected2", torrent.id);
continue 'torrent;
Expand Down Expand Up @@ -536,6 +551,7 @@ pub async fn select_torrents<T: Iterator<Item = MaMTorrent>>(
meta,
false,
false,
events,
)
.await?;
}
Expand Down Expand Up @@ -671,22 +687,36 @@ pub async fn add_metadata_only_torrent(
Ok(())
}

pub enum PreparedTorrentMetaUpdate {
Unchanged,
Silent,
Pending(Box<PendingTorrentMetaUpdate>),
}

pub struct PendingTorrentMetaUpdate {
torrent: mlm_db::Torrent,
meta: TorrentMeta,
diff: Vec<mlm_db::TorrentMetaDiff>,
mam_id: Option<u64>,
}

#[allow(clippy::too_many_arguments)]
pub async fn update_torrent_meta(
config: &Config,
db: &Database<'_>,
(guard, rw): (MutexGuard<'_, ()>, RwTransaction<'_>),
pub fn queue_torrent_meta_update(
rw: &RwTransaction<'_>,
mam_torrent: Option<&MaMTorrent>,
mut torrent: mlm_db::Torrent,
mut meta: TorrentMeta,
allow_non_mam: bool,
linker_is_owner: bool,
) -> Result<()> {
) -> Result<PreparedTorrentMetaUpdate> {
meta.ids.extend(torrent.meta.ids.clone());
meta.tags = torrent.meta.tags.clone();
if meta.description.is_empty() {
meta.description = torrent.meta.description.clone();
}
if meta == torrent.meta {
return Ok(PreparedTorrentMetaUpdate::Unchanged);
}

if !allow_non_mam && torrent.meta.source != MetadataSource::Mam {
// Update VIP status and uploaded_at still
Expand All @@ -695,10 +725,9 @@ pub async fn update_torrent_meta(
{
torrent.meta.vip_status = meta.vip_status;
torrent.meta.uploaded_at = meta.uploaded_at;
rw.upsert(torrent.clone())?;
rw.commit()?;
rw.upsert(torrent)?;
}
return Ok(());
return Ok(PreparedTorrentMetaUpdate::Silent);
}

// Check expiring VIP
Expand All @@ -713,9 +742,8 @@ pub async fn update_torrent_meta(
torrent.meta.vip_status = meta.vip_status.clone();
// If expiring VIP was the only change, just silently update the database
if torrent.meta == meta {
rw.upsert(torrent.clone())?;
rw.commit()?;
return Ok(());
rw.upsert(torrent)?;
return Ok(PreparedTorrentMetaUpdate::Silent);
}
}

Expand All @@ -725,9 +753,8 @@ pub async fn update_torrent_meta(
torrent.meta.num_files = meta.num_files;
// If uploaded_at or num_files was the only change, just silently update the database
if torrent.meta == meta {
rw.upsert(torrent.clone())?;
rw.commit()?;
return Ok(());
rw.upsert(torrent)?;
return Ok(PreparedTorrentMetaUpdate::Silent);
}
}

Expand All @@ -750,8 +777,29 @@ pub async fn update_torrent_meta(
torrent.meta = meta.clone();
torrent.title_search = normalize_title(&meta.title);
rw.upsert(torrent.clone())?;
rw.commit()?;
drop(guard);
Ok(PreparedTorrentMetaUpdate::Pending(Box::new(
PendingTorrentMetaUpdate {
torrent,
meta,
diff,
mam_id: mam_torrent.map(|m| m.id),
},
)))
}

pub async fn finalize_torrent_meta_update(
config: &Config,
db: &Database<'_>,
pending: PendingTorrentMetaUpdate,
events: &crate::stats::Events,
) -> Result<()> {
let PendingTorrentMetaUpdate {
torrent,
meta,
diff,
mam_id,
} = pending;
let id = torrent.id.clone();

if let Some(library_path) = &torrent.library_path
&& let serde_json::Value::Object(new) = abs::create_metadata(&meta)
Expand Down Expand Up @@ -782,9 +830,9 @@ pub async fn update_torrent_meta(
}

if !diff.is_empty() {
let mam_id = mam_torrent.map(|m| m.id);
write_event(
db,
events,
Event::new(
Some(id),
mam_id,
Expand All @@ -799,12 +847,45 @@ pub async fn update_torrent_meta(
Ok(())
}

#[allow(clippy::too_many_arguments)]
pub async fn update_torrent_meta(
config: &Config,
db: &Database<'_>,
(guard, rw): (MutexGuard<'_, ()>, RwTransaction<'_>),
mam_torrent: Option<&MaMTorrent>,
torrent: mlm_db::Torrent,
meta: TorrentMeta,
allow_non_mam: bool,
linker_is_owner: bool,
events: &crate::stats::Events,
) -> Result<()> {
let prepared = queue_torrent_meta_update(
&rw,
mam_torrent,
torrent,
meta,
allow_non_mam,
linker_is_owner,
)?;

if !matches!(prepared, PreparedTorrentMetaUpdate::Unchanged) {
rw.commit()?;
}
drop(guard);

if let PreparedTorrentMetaUpdate::Pending(pending) = prepared {
finalize_torrent_meta_update(config, db, *pending, events).await?;
}
Ok(())
}

async fn update_selected_torrent_meta(
db: &Database<'_>,
(guard, rw): (MutexGuard<'_, ()>, RwTransaction<'_>),
mam: &MaM<'_>,
torrent: SelectedTorrent,
meta: TorrentMeta,
events: &crate::stats::Events,
) -> Result<()> {
let mam_id = torrent.mam_id;
let diff = torrent.meta.diff(&meta);
Expand All @@ -824,6 +905,7 @@ async fn update_selected_torrent_meta(
drop(guard);
write_event(
db,
events,
Event::new(
hash,
Some(mam_id),
Expand Down
Loading