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