diff --git a/paperforge/pdf_resolver.py b/paperforge/pdf_resolver.py index 9b7460ec..2d576f50 100644 --- a/paperforge/pdf_resolver.py +++ b/paperforge/pdf_resolver.py @@ -60,7 +60,7 @@ def resolve_pdf_path( # Try Zotero storage-relative (format: "storage:XXXX/item.pdf") if raw.startswith("storage:") and zotero_dir is not None: storage_rel = raw[len("storage:") :].lstrip("/") - storage_candidate = (zotero_dir / storage_rel.replace("/", os.sep)).resolve() + storage_candidate = (zotero_dir / "storage" / storage_rel.replace("/", os.sep)).resolve() if is_valid_pdf(storage_candidate): return str(storage_candidate) diff --git a/paperforge/worker/base_views.py b/paperforge/worker/base_views.py index 7550e696..f344ee34 100644 --- a/paperforge/worker/base_views.py +++ b/paperforge/worker/base_views.py @@ -110,6 +110,9 @@ def build_base_views(domain: str) -> list[dict]: "file.name", "title", "year", + "first_author", + "journal", + "impact_factor", "has_pdf", "do_ocr", "analyze", @@ -125,6 +128,9 @@ def build_base_views(domain: str) -> list[dict]: "order": [ "year", "title", + "first_author", + "journal", + "impact_factor", "has_pdf", "do_ocr", "analyze", @@ -137,27 +143,27 @@ def build_base_views(domain: str) -> list[dict]: }, { "name": "待 OCR", - "order": ["year", "title", "has_pdf", "do_ocr", "ocr_status", "pdf_path"], + "order": ["year", "first_author", "title", "has_pdf", "do_ocr", "ocr_status", "pdf_path"], "filter": 'do_ocr = true AND ocr_status = "pending"', }, { "name": "OCR 完成", - "order": ["year", "title", "has_pdf", "do_ocr", "ocr_status", "pdf_path"], + "order": ["year", "first_author", "title", "has_pdf", "do_ocr", "ocr_status", "pdf_path"], "filter": 'ocr_status = "done"', }, { "name": "待深度阅读", - "order": ["year", "title", "has_pdf", "do_ocr", "analyze", "ocr_status", "deep_reading_status", "pdf_path"], + "order": ["year", "first_author", "title", "has_pdf", "do_ocr", "analyze", "ocr_status", "deep_reading_status", "pdf_path"], "filter": 'analyze = true AND ocr_status = "done" AND deep_reading_status = "pending"', }, { "name": "深度阅读完成", - "order": ["year", "title", "has_pdf", "do_ocr", "analyze", "ocr_status", "deep_reading_status", "pdf_path"], + "order": ["year", "first_author", "title", "has_pdf", "do_ocr", "analyze", "ocr_status", "deep_reading_status", "pdf_path"], "filter": 'deep_reading_status = "done"', }, { "name": "正式卡片", - "order": ["title", "year", "has_pdf", "deep_reading_status", "pdf_path"], + "order": ["title", "year", "first_author", "journal", "impact_factor", "has_pdf", "deep_reading_status", "pdf_path"], "filter": 'deep_reading_status = "done"', }, { @@ -165,6 +171,9 @@ def build_base_views(domain: str) -> list[dict]: "order": [ "title", "year", + "first_author", + "journal", + "impact_factor", "has_pdf", "do_ocr", "analyze", @@ -245,6 +254,12 @@ def merge_base_views(existing_content: str | None, new_views: list[dict]) -> str displayName: "Title" year: displayName: "Year" + first_author: + displayName: "First Author" + journal: + displayName: "Journal" + impact_factor: + displayName: "IF" has_pdf: displayName: "PDF" do_ocr: @@ -374,6 +389,12 @@ properties: displayName: "Title" year: displayName: "Year" + first_author: + displayName: "First Author" + journal: + displayName: "Journal" + impact_factor: + displayName: "IF" has_pdf: displayName: "PDF" do_ocr: diff --git a/paperforge/worker/sync.py b/paperforge/worker/sync.py index b87f794c..55e0022c 100644 --- a/paperforge/worker/sync.py +++ b/paperforge/worker/sync.py @@ -279,7 +279,7 @@ def obsidian_wikilink_for_pdf(pdf_path: str, vault_dir: Path, zotero_dir: Path | # Handle storage: prefix paths by resolving through zotero_dir if text.startswith("storage:") and zotero_dir is not None: storage_rel = text[len("storage:") :].lstrip("/").lstrip("\\") - absolute_pdf_path = (zotero_dir / storage_rel.replace("/", os.sep)).resolve() + absolute_pdf_path = (zotero_dir / "storage" / storage_rel.replace("/", os.sep)).resolve() absolute_str = str(absolute_pdf_path) else: absolute_str = absolutize_vault_path(vault_dir, text, resolve_junction=True) @@ -491,8 +491,10 @@ def load_export_rows(path: Path) -> list[dict]: "key": item.get("key") or item.get("itemKey", ""), "title": item.get("title", ""), "authors": extract_authors(item), + "creators": item.get("creators", []), "abstract": item.get("abstractNote", ""), "journal": item.get("publicationTitle", ""), + "extra": item.get("extra", ""), "year": _extract_year(item.get("date", "")), "date": item.get("date", ""), "doi": item.get("DOI", ""), @@ -888,7 +890,7 @@ def run_selection_sync(vault: Path, verbose: bool = False) -> int: if c.get("creatorType") == "author": first_author = f"{c.get('firstName', '')} {c.get('lastName', '')}".strip() break - journal = item.get("publicationTitle", "") + journal = item.get("journal", "") extra = item.get("extra", "") impact_factor = lookup_impact_factor(journal, extra, vault) # Convert supplementary storage: paths to wikilinks diff --git a/tests/test_path_normalization.py b/tests/test_path_normalization.py index f3b4e045..cabf8934 100644 --- a/tests/test_path_normalization.py +++ b/tests/test_path_normalization.py @@ -197,18 +197,17 @@ class TestWikilinkGeneration: """Tests for obsidian_wikilink_for_pdf() wikilink generation.""" def test_basic_wikilink(self, tmp_path: Path) -> None: - """storage:KEY/file.pdf resolves to [[system/Zotero/KEY/file.pdf]].""" + """storage:KEY/file.pdf resolves to [[system/Zotero/storage/KEY/file.pdf]].""" vault_dir = tmp_path / "vault" zotero_dir = vault_dir / "system" / "Zotero" - # Current implementation joins storage: directly under zotero_dir - storage_dir = zotero_dir / "KEY" + storage_dir = zotero_dir / "storage" / "KEY" storage_dir.mkdir(parents=True) pdf = storage_dir / "file.pdf" pdf.write_text("PDF content") result = obsidian_wikilink_for_pdf("storage:KEY/file.pdf", vault_dir, zotero_dir) - assert result == "[[system/Zotero/KEY/file.pdf]]" + assert result == "[[system/Zotero/storage/KEY/file.pdf]]" def test_junction_resolution(self, tmp_path: Path, monkeypatch) -> None: """Junction resolved before relative path computed.""" @@ -239,8 +238,7 @@ class TestWikilinkGeneration: """Output wikilink uses forward slashes, never backslashes.""" vault_dir = tmp_path / "vault" zotero_dir = vault_dir / "system" / "Zotero" - # Current implementation joins storage: directly under zotero_dir - storage_dir = zotero_dir / "KEY" + storage_dir = zotero_dir / "storage" / "KEY" storage_dir.mkdir(parents=True) pdf = storage_dir / "file.pdf" pdf.write_text("PDF content") @@ -254,8 +252,7 @@ class TestWikilinkGeneration: """Chinese characters preserved in wikilink without escaping.""" vault_dir = tmp_path / "vault" zotero_dir = vault_dir / "system" / "Zotero" - # Current implementation joins storage: directly under zotero_dir - storage_dir = zotero_dir / "KEY" + storage_dir = zotero_dir / "storage" / "KEY" storage_dir.mkdir(parents=True) pdf = storage_dir / "中文论文.pdf" pdf.write_text("PDF content") @@ -263,7 +260,7 @@ class TestWikilinkGeneration: result = obsidian_wikilink_for_pdf("storage:KEY/中文论文.pdf", vault_dir, zotero_dir) assert "中文论文" in result - assert result == "[[system/Zotero/KEY/中文论文.pdf]]" + assert result == "[[system/Zotero/storage/KEY/中文论文.pdf]]" def test_empty_pdf_path(self, tmp_path: Path) -> None: """Empty pdf_path returns empty string.""" diff --git a/tests/test_pdf_resolver.py b/tests/test_pdf_resolver.py index ec0a76a5..0080c2cb 100644 --- a/tests/test_pdf_resolver.py +++ b/tests/test_pdf_resolver.py @@ -50,9 +50,9 @@ class TestResolvePdfPath: assert result == str(target.resolve()) def test_zotero_storage_relative_path(self, tmp_path: Path) -> None: - """storage: prefixed path resolves against zotero_dir.""" + """storage: prefixed path resolves against zotero_dir/storage/.""" zotero_dir = tmp_path / "zotero" - storage_dir = zotero_dir / "ABC123" + storage_dir = zotero_dir / "storage" / "ABC123" storage_dir.mkdir(parents=True) pdf = storage_dir / "item.pdf" pdf.write_text("PDF content")