initial commit
This commit is contained in:
@@ -0,0 +1,165 @@
|
|||||||
|
# Obsidian Import Utility
|
||||||
|
|
||||||
|
Import **Markdown** and **PDF** files into an [Obsidian](https://obsidian.md) vault with a single click.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **.md files**
|
||||||
|
- Copies contents into a new Obsidian note.
|
||||||
|
- Uses the filename as the note title (prepended as `# Title`).
|
||||||
|
|
||||||
|
- **.pdf files**
|
||||||
|
- Converts PDF text into a markdown note.
|
||||||
|
- Extracts embedded images and embeds them in the note.
|
||||||
|
- Creates a **hidden folder** per file (e.g. `.myfile_assets/`) containing:
|
||||||
|
- The original PDF
|
||||||
|
- All extracted images
|
||||||
|
- Links back to the original PDF from the note.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Install Python 3 (usually already present on Linux).
|
||||||
|
2. Install the required dependency:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Or directly:
|
||||||
|
```bash
|
||||||
|
pip install pymupdf
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Tell the script where your Obsidian vault lives. Choose **one** method:
|
||||||
|
|
||||||
|
- **Environment variable** (recommended for Thunar):
|
||||||
|
```bash
|
||||||
|
export OBSIDIAN_VAULT="$HOME/Obsidian/MyVault"
|
||||||
|
```
|
||||||
|
- **Command-line flag**:
|
||||||
|
```bash
|
||||||
|
python main.py --vault ~/Obsidian/MyVault /path/to/file.pdf
|
||||||
|
```
|
||||||
|
- **Auto-detect**: if you have exactly one folder inside `~/Obsidian`, it will be picked automatically.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### From the terminal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Markdown
|
||||||
|
python main.py /path/to/notes.md
|
||||||
|
|
||||||
|
# PDF
|
||||||
|
python main.py /path/to/document.pdf
|
||||||
|
|
||||||
|
# With explicit vault
|
||||||
|
python main.py --vault ~/Obsidian/Work /path/to/slides.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### With the wrapper script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./import-to-obsidian.sh /path/to/file.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `VAULT_PATH=""` inside `import-to-obsidian.sh` if you prefer a hard-coded default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thunar Custom Action
|
||||||
|
|
||||||
|
To add a right-click menu entry in **Thunar** (Xfce file manager):
|
||||||
|
|
||||||
|
### Option A – Using the Thunar GUI
|
||||||
|
|
||||||
|
1. Open Thunar.
|
||||||
|
2. Go to **Edit → Configure custom actions…**
|
||||||
|
3. Click **+** (Add a new custom action).
|
||||||
|
4. Fill in the fields:
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Name** | Import to Obsidian |
|
||||||
|
| **Description** | Import selected file into Obsidian vault |
|
||||||
|
| **Command** | `/usr/bin/python3 /full/path/to/main.py --vault /home/USER/Obsidian/MyVault %F` |
|
||||||
|
| **Icon** | `folder-documents` (or any icon you like) |
|
||||||
|
|
||||||
|
5. Switch to the **Appearance Conditions** tab.
|
||||||
|
6. Check **Text files** and **Other files** (or just `*.md;*.pdf` if you want to restrict it).
|
||||||
|
7. Click **OK**.
|
||||||
|
|
||||||
|
> **Tip:** `%F` passes all selected files. If you want the action to appear only for single selections you can use `%f` instead.
|
||||||
|
|
||||||
|
### Option B – Editing `uca.xml` directly
|
||||||
|
|
||||||
|
Paste the following snippet into `~/.config/Thunar/uca.xml` inside the `<actions>` block (adjust paths):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<action>
|
||||||
|
<icon>folder-documents</icon>
|
||||||
|
<name>Import to Obsidian</name>
|
||||||
|
<command>/usr/bin/python3 /home/USER/Code/projects/obsidianUtils/importFileToObsidian/main.py --vault /home/USER/Obsidian/MyVault %F</command>
|
||||||
|
<description>Import selected file(s) into Obsidian vault</description>
|
||||||
|
<patterns>*.md;*.pdf</patterns>
|
||||||
|
<other-files/>
|
||||||
|
<text-files/>
|
||||||
|
</action>
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart Thunar or press `Ctrl+Shift+R` inside a Thunar window to reload custom actions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variable in `.bashrc`
|
||||||
|
|
||||||
|
If you want the vault path to be available everywhere (including Thunar when launched from your shell), add this to `~/.bashrc` or `~/.profile`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OBSIDIAN_VAULT="$HOME/Obsidian/MyVault"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: Thunar started from a graphical session (not from a terminal) may not see variables defined in `.bashrc`. In that case, hard-code the `--vault` flag in the custom action command.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure After Import
|
||||||
|
|
||||||
|
### Markdown import
|
||||||
|
|
||||||
|
```
|
||||||
|
MyVault/
|
||||||
|
└── notes.md # imported note with '# notes' as title
|
||||||
|
```
|
||||||
|
|
||||||
|
### PDF import
|
||||||
|
|
||||||
|
```
|
||||||
|
MyVault/
|
||||||
|
├── document.md
|
||||||
|
└── .document_assets/
|
||||||
|
├── document.pdf
|
||||||
|
├── page_1_img_1.png
|
||||||
|
├── page_1_img_2.jpg
|
||||||
|
└── page_2_img_1.png
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated `document.md` contains:
|
||||||
|
- The extracted text per page.
|
||||||
|
- Embedded images under an "Extracted Images" section.
|
||||||
|
- A link back to the original PDF inside the hidden assets folder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Problem | Solution |
|
||||||
|
|---------|----------|
|
||||||
|
| `Vault path does not exist` | Double-check `--vault` or `OBSIDIAN_VAULT`. |
|
||||||
|
| `PDF handling requires 'pymupdf'` | Run `pip install pymupdf`. |
|
||||||
|
| Thunar action does not appear | Make sure the file pattern matches (`*.md;*.pdf`) and restart Thunar (`Ctrl+Shift+R`). |
|
||||||
|
| Images are missing in the note | `pymupdf` extracts raw embedded images; vector graphics or scanned pages may not yield separate image files. |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT (or choose your own).
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,29 @@
|
|||||||
|
"""Handler for importing Markdown files into Obsidian."""
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from utils.config import ensure_unique_path, sanitize_filename
|
||||||
|
|
||||||
|
|
||||||
|
def handle_markdown(file_path: Path, vault_path: Path) -> Path:
|
||||||
|
"""Import a Markdown file into the Obsidian vault.
|
||||||
|
|
||||||
|
- Copies contents to a new note.
|
||||||
|
- Uses the filename (without extension) as the note title.
|
||||||
|
- Prepends a title heading if the file does not already start with it.
|
||||||
|
"""
|
||||||
|
title = sanitize_filename(file_path.stem)
|
||||||
|
target_name = f"{title}.md"
|
||||||
|
target_path = ensure_unique_path(vault_path / target_name)
|
||||||
|
|
||||||
|
content = file_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# Prepend title if the file doesn't already start with it as an H1
|
||||||
|
first_line = content.splitlines()[0] if content.strip() else ""
|
||||||
|
if not first_line.strip().startswith(f"# {title}"):
|
||||||
|
content = f"# {title}\n\n{content}"
|
||||||
|
|
||||||
|
target_path.write_text(content, encoding="utf-8")
|
||||||
|
print(f"[MD] Created note: {target_path}")
|
||||||
|
return target_path
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""Handler for importing PDF files into Obsidian."""
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from utils.config import ensure_unique_path, sanitize_filename
|
||||||
|
|
||||||
|
|
||||||
|
def handle_pdf(file_path: Path, vault_path: Path) -> Path:
|
||||||
|
"""Import a PDF file into the Obsidian vault.
|
||||||
|
|
||||||
|
- Converts PDF content to a new Obsidian markdown note.
|
||||||
|
- Creates a hidden folder for extracted images and the original PDF.
|
||||||
|
- Embeds extracted images and links to the original PDF.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import fitz # pymupdf
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"PDF handling requires 'pymupdf'. Install it with:\n pip install pymupdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
title = sanitize_filename(file_path.stem)
|
||||||
|
note_name = f"{title}.md"
|
||||||
|
note_path = ensure_unique_path(vault_path / note_name)
|
||||||
|
|
||||||
|
# Hidden assets folder: .{title}_assets
|
||||||
|
assets_folder_name = f".{title}_assets"
|
||||||
|
assets_folder = vault_path / assets_folder_name
|
||||||
|
counter = 1
|
||||||
|
while assets_folder.exists():
|
||||||
|
assets_folder_name = f".{title}_assets_{counter}"
|
||||||
|
assets_folder = vault_path / assets_folder_name
|
||||||
|
counter += 1
|
||||||
|
assets_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Copy original PDF into assets folder
|
||||||
|
pdf_copy_name = f"{title}.pdf"
|
||||||
|
pdf_copy_path = ensure_unique_path(assets_folder / pdf_copy_name)
|
||||||
|
shutil.copy2(file_path, pdf_copy_path)
|
||||||
|
|
||||||
|
# Open PDF and extract content + images
|
||||||
|
doc = fitz.open(file_path)
|
||||||
|
markdown_lines = [f"# {title}", ""]
|
||||||
|
|
||||||
|
# Link to original PDF
|
||||||
|
rel_pdf_path = f"{assets_folder_name}/{pdf_copy_path.name}"
|
||||||
|
markdown_lines.append(f"**Original PDF:** [{pdf_copy_path.name}]({rel_pdf_path})")
|
||||||
|
markdown_lines.append("")
|
||||||
|
|
||||||
|
# Extract images and text per page
|
||||||
|
image_entries = [] # list of (page_num, img_path, rel_path)
|
||||||
|
for page_num in range(len(doc)):
|
||||||
|
page = doc.load_page(page_num)
|
||||||
|
|
||||||
|
# Extract images
|
||||||
|
img_list = page.get_images(full=True)
|
||||||
|
for img_index, img in enumerate(img_list):
|
||||||
|
xref = img[0]
|
||||||
|
try:
|
||||||
|
base_image = doc.extract_image(xref)
|
||||||
|
image_bytes = base_image["image"]
|
||||||
|
ext = base_image["ext"]
|
||||||
|
img_filename = f"page_{page_num + 1}_img_{img_index + 1}.{ext}"
|
||||||
|
img_path = assets_folder / img_filename
|
||||||
|
# Handle duplicates
|
||||||
|
img_path = ensure_unique_path(img_path)
|
||||||
|
img_path.write_bytes(image_bytes)
|
||||||
|
rel_img = f"{assets_folder_name}/{img_path.name}"
|
||||||
|
image_entries.append((page_num + 1, img_path, rel_img))
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"[PDF] Warning: could not extract image {img_index} on page {page_num + 1}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract text
|
||||||
|
text = page.get_text()
|
||||||
|
if text.strip():
|
||||||
|
markdown_lines.append(f"## Page {page_num + 1}")
|
||||||
|
markdown_lines.append("")
|
||||||
|
markdown_lines.append(text.strip())
|
||||||
|
markdown_lines.append("")
|
||||||
|
|
||||||
|
doc.close()
|
||||||
|
|
||||||
|
# Add image section if any images were extracted
|
||||||
|
if image_entries:
|
||||||
|
markdown_lines.append("## Extracted Images")
|
||||||
|
markdown_lines.append("")
|
||||||
|
for page_num, img_path, rel_img in image_entries:
|
||||||
|
markdown_lines.append(f"### Page {page_num}")
|
||||||
|
markdown_lines.append(f"")
|
||||||
|
markdown_lines.append("")
|
||||||
|
|
||||||
|
note_path.write_text("\n".join(markdown_lines), encoding="utf-8")
|
||||||
|
print(f"[PDF] Created note: {note_path}")
|
||||||
|
print(f"[PDF] Assets folder: {assets_folder}")
|
||||||
|
return note_path
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Wrapper script for importing files into Obsidian.
|
||||||
|
# Adjust VAULT_PATH below or leave empty to rely on ~/.bashrc / env / auto-detection.
|
||||||
|
|
||||||
|
VAULT_PATH=""
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
if [ -n "$VAULT_PATH" ]; then
|
||||||
|
python3 "$SCRIPT_DIR/main.py" --vault "$VAULT_PATH" "$@"
|
||||||
|
else
|
||||||
|
python3 "$SCRIPT_DIR/main.py" "$@"
|
||||||
|
fi
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Obsidian Import Utility
|
||||||
|
|
||||||
|
Import files into an Obsidian vault:
|
||||||
|
- .md -> copy contents to a new note (filename as title)
|
||||||
|
- .pdf -> convert to markdown, extract images, link original PDF
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python main.py /path/to/file.pdf
|
||||||
|
python main.py --vault ~/Obsidian/MyVault /path/to/file.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from handlers.md_handler import handle_markdown
|
||||||
|
from handlers.pdf_handler import handle_pdf
|
||||||
|
from utils.config import get_vault_path
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Import files into an Obsidian vault.")
|
||||||
|
parser.add_argument("file", help="Path to the file to import.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--vault",
|
||||||
|
dest="vault",
|
||||||
|
default=None,
|
||||||
|
help="Path to the Obsidian vault. Overrides OBSIDIAN_VAULT env var.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
file_path = Path(args.file).expanduser().resolve()
|
||||||
|
if not file_path.exists():
|
||||||
|
print(f"Error: file not found: {file_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
vault_path = get_vault_path(args.vault)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
suffix = file_path.suffix.lower()
|
||||||
|
|
||||||
|
if suffix == ".md":
|
||||||
|
handle_markdown(file_path, vault_path)
|
||||||
|
elif suffix == ".pdf":
|
||||||
|
handle_pdf(file_path, vault_path)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"Error: unsupported file type '{suffix}'. Supported: .md, .pdf",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pymupdf
|
||||||
Binary file not shown.
@@ -0,0 +1,63 @@
|
|||||||
|
"""Configuration and common utilities for Obsidian import."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "obsidian-import" / "config.ini"
|
||||||
|
|
||||||
|
|
||||||
|
def get_vault_path(cli_vault: Optional[str] = None) -> Path:
|
||||||
|
"""Resolve the Obsidian vault path from CLI arg, env var, or default locations."""
|
||||||
|
if cli_vault:
|
||||||
|
vault = Path(cli_vault).expanduser().resolve()
|
||||||
|
if not vault.exists():
|
||||||
|
raise FileNotFoundError(f"Vault path does not exist: {vault}")
|
||||||
|
return vault
|
||||||
|
|
||||||
|
env_vault = os.environ.get("OBSIDIAN_VAULT")
|
||||||
|
if env_vault:
|
||||||
|
vault = Path(env_vault).expanduser().resolve()
|
||||||
|
if vault.exists():
|
||||||
|
return vault
|
||||||
|
|
||||||
|
# Try common default locations
|
||||||
|
obsidian_dir = Path.home() / "Obsidian"
|
||||||
|
if obsidian_dir.exists():
|
||||||
|
vaults = [d for d in obsidian_dir.iterdir() if d.is_dir()]
|
||||||
|
if len(vaults) == 1:
|
||||||
|
return vaults[0]
|
||||||
|
elif len(vaults) > 1:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Multiple vaults found in {obsidian_dir}. "
|
||||||
|
"Set OBSIDIAN_VAULT or use --vault."
|
||||||
|
)
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
"Could not determine Obsidian vault path. "
|
||||||
|
"Use --vault or set OBSIDIAN_VAULT environment variable."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_filename(name: str) -> str:
|
||||||
|
"""Remove or replace characters unsafe for filenames."""
|
||||||
|
invalid = '<>:"/\\|?*'
|
||||||
|
for ch in invalid:
|
||||||
|
name = name.replace(ch, '_')
|
||||||
|
return name.strip('. ')
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_unique_path(target: Path) -> Path:
|
||||||
|
"""If target exists, append a counter to make it unique."""
|
||||||
|
if not target.exists():
|
||||||
|
return target
|
||||||
|
stem = target.stem
|
||||||
|
suffix = target.suffix
|
||||||
|
parent = target.parent
|
||||||
|
counter = 1
|
||||||
|
while True:
|
||||||
|
new_name = f"{stem}_{counter}{suffix}"
|
||||||
|
candidate = parent / new_name
|
||||||
|
if not candidate.exists():
|
||||||
|
return candidate
|
||||||
|
counter += 1
|
||||||
Reference in New Issue
Block a user