initial commit

This commit is contained in:
2026-05-26 14:27:01 +02:00
commit ef411fbe23
11 changed files with 427 additions and 0 deletions
+165
View File
@@ -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).
@@ -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"![{img_path.name}]({rel_img})")
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
+12
View File
@@ -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
+59
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
pymupdf
+63
View File
@@ -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