// src/services/batch/asset/helpers.rs // // 批量处理各步骤的独立函数,从 process_single_bibcode 提取, // 降低单文件行数并提高可测试性。 use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::Arc; use sqlx::SqlitePool; use tokio::sync::Mutex; use crate::services::download::Downloader; use crate::Config; use super::{AssetBatchStatus, BatchAction}; /// 执行单篇文献的下载步骤。 /// /// 若成功下载 PDF 或 HTML 任一格式,更新 `pdf_path` / `html_path` 并写回数据库。 /// 返回 true 表示下载成功(至少一个文件),false 表示全部失败。 #[allow(clippy::too_many_arguments)] pub(super) async fn run_download_step( bibcode: &str, action: BatchAction, db: &SqlitePool, config: &Config, downloader: &Arc, status: &Arc>, dl_count: &AtomicI32, dl_failed_count: &AtomicI32, arxiv_id: &str, doi: &str, pdf_path: &mut Option, html_path: &mut Option, ) -> bool { // 仅当 action 包含下载时执行 if action != BatchAction::Download && action != BatchAction::All { return true; // 无需下载,视为"成功" } let is_pdf_exist = pdf_path .as_ref() .map(|p| config.library_dir.join(p).exists()) .unwrap_or(false); let is_html_exist = html_path .as_ref() .map(|p| config.library_dir.join(p).exists()) .unwrap_or(false); if is_pdf_exist || is_html_exist { { let mut s = status.lock().await; s.add_log(format!( "文献 {} 本地已存在 PDF 或 HTML,跳过下载。", bibcode )); } let _c = dl_count.fetch_add(1, Ordering::SeqCst) + 1; { let mut s = status.lock().await; s.downloaded = _c; } return true; } // 需要执行下载 { let mut s = status.lock().await; s.add_log(format!("文献 {} 本地无 PDF/HTML,开始下载...", bibcode)); } let (pdf_res, html_res) = if !arxiv_id.is_empty() { let res = downloader .download_arxiv_direct(arxiv_id, &config.library_dir) .await; if res.0.is_ok() || res.1.is_ok() { res } else { { let mut s = status.lock().await; s.add_log(format!( "文献 {} arXiv 通道下载失败,回退尝试 ADS/出版商下载...", bibcode )); } let doi_opt = if !doi.is_empty() { Some(doi) } else { None }; downloader .download_paper(bibcode, doi_opt, &config.library_dir) .await } } else { let doi_opt = if !doi.is_empty() { Some(doi) } else { None }; downloader .download_paper(bibcode, doi_opt, &config.library_dir) .await }; if pdf_res.is_ok() || html_res.is_ok() { let pdf_rel = match pdf_res { Ok(p) => Some( p.strip_prefix(&config.library_dir) .unwrap_or(&p) .to_string_lossy() .to_string(), ), Err(_) => None, }; let html_rel = match html_res { Ok(p) => Some( p.strip_prefix(&config.library_dir) .unwrap_or(&p) .to_string_lossy() .to_string(), ), Err(_) => None, }; *pdf_path = pdf_rel.clone(); *html_path = html_rel.clone(); let _ = sqlx::query("UPDATE papers SET pdf_path = ?, html_path = ? WHERE bibcode = ?") .bind(pdf_rel) .bind(html_rel) .bind(bibcode) .execute(db) .await; let _c = dl_count.fetch_add(1, Ordering::SeqCst) + 1; { let mut s = status.lock().await; s.downloaded = _c; s.add_log(format!("文献 {} 下载成功!", bibcode)); } true } else { let _f = dl_failed_count.fetch_add(1, Ordering::SeqCst) + 1; let mut s = status.lock().await; s.download_failed = _f; let pdf_err = match pdf_res { Err(e) => format!("error: {}", e), _ => "error: 未知错误".to_string(), }; let html_err = match html_res { Err(e) => format!("error: {}", e), _ => "error: 未知错误".to_string(), }; s.add_log(format!( "文献 {} 下载失败。PDF: {}, HTML: {}", bibcode, pdf_err, html_err )); let _ = sqlx::query("UPDATE papers SET pdf_path = ?, html_path = ? WHERE bibcode = ?") .bind(&pdf_err) .bind(&html_err) .bind(bibcode) .execute(db) .await; false } } /// 生成文献的源链接 URL(优先 ADS 标准 bibcode,其次 arXiv)。 pub(super) fn build_source_url(bibcode: &str, arxiv_id: &str) -> String { if bibcode.len() == 19 { format!("https://ui.adsabs.harvard.edu/abs/{}/abstract", bibcode) } else if !arxiv_id.is_empty() { format!( "https://ui.adsabs.harvard.edu/abs/arXiv:{}/abstract", arxiv_id ) } else { format!("https://ui.adsabs.harvard.edu/abs/{}/abstract", bibcode) } } /// 为已解析的 Markdown 内容构建 YAML frontmatter 头部。 pub(super) async fn build_markdown_with_frontmatter( db: &SqlitePool, bibcode: &str, md_body: &str, source_url: &str, ) -> Option { use sqlx::Row; let row = sqlx::query("SELECT title, authors, pub, year, keywords FROM papers WHERE bibcode = ?") .bind(bibcode) .fetch_optional(db) .await .ok()??; let title: String = row.get(0); let authors_json: String = row.get(1); let pub_journal: String = row.get(2); let year: String = row.get(3); let keywords_json: String = row.get(4); let authors: Vec = serde_json::from_str(&authors_json).unwrap_or_default(); let keywords: Vec = serde_json::from_str(&keywords_json).unwrap_or_default(); let front_matter = format!( "---\ntitle: {}\nauthor: [{}]\npublisher: {}\nsource: \"{}\"\ndate: \"{}\"\ntags: \"{}\"\n---\n\n", serde_json::to_string(&title).unwrap_or_else(|_| format!("\"{}\"", title)), authors .iter() .map(|a| format!("\"{}\"", a)) .collect::>() .join(", "), serde_json::to_string(&pub_journal) .unwrap_or_else(|_| format!("\"{}\"", pub_journal)), source_url, year, keywords.join(",") ); Some(format!("{}{}", front_matter, md_body)) } /// 将 Markdown 内容写入 library_dir 并更新数据库 markdown_path。 pub(super) fn write_markdown_and_update_db( config: &Config, bibcode: &str, content: &str, ) -> Option { let md_filename = format!("{}.md", bibcode); let md_dest = config.library_dir.join("Markdown").join(&md_filename); if let Some(parent) = md_dest.parent() { let _ = std::fs::create_dir_all(parent); } if std::fs::write(&md_dest, content).is_ok() { Some(format!("Markdown/{}", md_filename)) } else { None } }