chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Metadata Adder for Obsidian Vault
|
||||
Adds standardized frontmatter to markdown files that lack it.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
|
||||
class MetadataAdder:
|
||||
def __init__(self, vault_path):
|
||||
self.vault_path = Path(vault_path)
|
||||
self.stats = {
|
||||
'processed': 0,
|
||||
'updated': 0,
|
||||
'skipped': 0,
|
||||
'errors': 0
|
||||
}
|
||||
|
||||
def get_file_creation_date(self, file_path):
|
||||
"""Get file creation date from filesystem."""
|
||||
try:
|
||||
stat = os.stat(file_path)
|
||||
# Use birthtime on macOS, ctime on others
|
||||
timestamp = stat.st_birthtime if hasattr(stat, 'st_birthtime') else stat.st_ctime
|
||||
return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d')
|
||||
except:
|
||||
return datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
def determine_file_type(self, file_path):
|
||||
"""Determine the type of note based on path and content."""
|
||||
path_str = str(file_path).lower()
|
||||
|
||||
if 'moc' in path_str or 'map of content' in path_str:
|
||||
return 'map-of-content'
|
||||
elif 'daily notes' in path_str or 'daily note' in path_str:
|
||||
return 'daily'
|
||||
elif 'research' in path_str or 'articles' in path_str:
|
||||
return 'research'
|
||||
elif 'client' in path_str or 'camrohn llc' in path_str:
|
||||
return 'client-work'
|
||||
elif 'tutorial' in path_str or 'course' in path_str:
|
||||
return 'tutorial'
|
||||
elif 'idea' in path_str:
|
||||
return 'idea'
|
||||
elif 'meeting' in path_str:
|
||||
return 'meeting'
|
||||
elif 'email' in path_str:
|
||||
return 'email'
|
||||
else:
|
||||
return 'note'
|
||||
|
||||
def generate_tags_from_path(self, file_path):
|
||||
"""Generate tags based on file path."""
|
||||
tags = []
|
||||
path_parts = file_path.relative_to(self.vault_path).parts[:-1] # Exclude filename
|
||||
|
||||
# Map directory names to tags
|
||||
tag_mapping = {
|
||||
'ai development': 'ai/development',
|
||||
'ai articles': 'ai/research',
|
||||
'ai courses': 'tutorial/course',
|
||||
'ai ideas': 'idea',
|
||||
'camrohn llc': 'client',
|
||||
'daily notes': 'daily',
|
||||
'clippings': 'clippings',
|
||||
'mcp': 'mcp',
|
||||
'langchain': 'langchain',
|
||||
'graphrag': 'graphrag'
|
||||
}
|
||||
|
||||
for part in path_parts:
|
||||
part_lower = part.lower()
|
||||
for key, tag in tag_mapping.items():
|
||||
if key in part_lower:
|
||||
tags.append(tag)
|
||||
break
|
||||
|
||||
# Add date tags for daily notes
|
||||
if 'daily' in tags:
|
||||
created_date = self.get_file_creation_date(file_path)
|
||||
year_month = datetime.strptime(created_date, '%Y-%m-%d').strftime('%Y/%m')
|
||||
tags.append(f'daily/{year_month}')
|
||||
|
||||
return list(set(tags)) # Remove duplicates
|
||||
|
||||
def has_frontmatter(self, content):
|
||||
"""Check if content already has frontmatter."""
|
||||
return content.strip().startswith('---')
|
||||
|
||||
def create_frontmatter(self, file_path, existing_content):
|
||||
"""Create appropriate frontmatter for the file."""
|
||||
created_date = self.get_file_creation_date(file_path)
|
||||
file_type = self.determine_file_type(file_path)
|
||||
tags = self.generate_tags_from_path(file_path)
|
||||
|
||||
# Extract title from first heading or filename
|
||||
title_match = re.search(r'^#\s+(.+)$', existing_content, re.MULTILINE)
|
||||
if title_match:
|
||||
title = title_match.group(1)
|
||||
else:
|
||||
title = file_path.stem.replace('_', ' ').replace('-', ' ')
|
||||
|
||||
frontmatter = f"""---
|
||||
tags: {tags}
|
||||
type: {file_type}
|
||||
created: {created_date}
|
||||
modified: {datetime.now().strftime('%Y-%m-%d')}
|
||||
status: active
|
||||
related: []
|
||||
aliases: []
|
||||
---
|
||||
|
||||
"""
|
||||
|
||||
return frontmatter
|
||||
|
||||
def process_file(self, file_path):
|
||||
"""Process a single markdown file."""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
if self.has_frontmatter(content):
|
||||
self.stats['skipped'] += 1
|
||||
return False
|
||||
|
||||
# Create and prepend frontmatter
|
||||
frontmatter = self.create_frontmatter(file_path, content)
|
||||
new_content = frontmatter + content
|
||||
|
||||
# Write back to file
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
self.stats['updated'] += 1
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing {file_path}: {e}")
|
||||
self.stats['errors'] += 1
|
||||
return False
|
||||
|
||||
def process_vault(self, dry_run=False):
|
||||
"""Process all markdown files in the vault."""
|
||||
# Directories to skip
|
||||
skip_dirs = {'.obsidian', '.trash', 'System_Files', '.git'}
|
||||
|
||||
for file_path in self.vault_path.rglob('*.md'):
|
||||
# Skip files in excluded directories
|
||||
if any(skip_dir in file_path.parts for skip_dir in skip_dirs):
|
||||
continue
|
||||
|
||||
self.stats['processed'] += 1
|
||||
|
||||
if dry_run:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
if not self.has_frontmatter(content):
|
||||
print(f"Would update: {file_path.relative_to(self.vault_path)}")
|
||||
self.stats['updated'] += 1
|
||||
else:
|
||||
self.stats['skipped'] += 1
|
||||
else:
|
||||
if self.process_file(file_path):
|
||||
print(f"Updated: {file_path.relative_to(self.vault_path)}")
|
||||
|
||||
return self.stats
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Add metadata to Obsidian vault files')
|
||||
parser.add_argument('--vault', default='/Users/cam/VAULT01',
|
||||
help='Path to Obsidian vault')
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
help='Show what would be updated without making changes')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
adder = MetadataAdder(args.vault)
|
||||
print(f"Processing vault at: {args.vault}")
|
||||
print("Dry run mode" if args.dry_run else "Making changes")
|
||||
print("-" * 50)
|
||||
|
||||
stats = adder.process_vault(dry_run=args.dry_run)
|
||||
|
||||
print("-" * 50)
|
||||
print(f"Files processed: {stats['processed']}")
|
||||
print(f"Files updated: {stats['updated']}")
|
||||
print(f"Files skipped (already have metadata): {stats['skipped']}")
|
||||
print(f"Errors: {stats['errors']}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user