Skip to content

Instantly share code, notes, and snippets.

@drosenstark
Created August 7, 2023 15:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save drosenstark/7bb1a6a84c3185614ecbf5c96dd26e73 to your computer and use it in GitHub Desktop.
Save drosenstark/7bb1a6a84c3185614ecbf5c96dd26e73 to your computer and use it in GitHub Desktop.
Obsidian Pico Publish, for Publishing Obsidian Notes
#!/usr/bin/env python3
import os
import json
import re
import fnmatch
from pathlib import Path
#####################
# Find Stuff to Run
#####################
def check_ssh_host(host):
ssh_config_file = os.path.expanduser("~/.ssh/config")
with open(ssh_config_file, "r") as file:
return host in file.read()
def get_publish_vault_directory():
vault_directory = os.getenv('OBSIDIAN_PICO_PUBLISH_VAULT_DIRECTORY')
if vault_directory is None:
print("Error: Environment variable OBSIDIAN_PICO_PUBLISH_VAULT_DIRECTORY is not set.")
print("Please set it using the following command:")
print("export OBSIDIAN_PICO_PUBLISH_VAULT_DIRECTORY=\"$HOME/Library/Mobile Documents/iCloud~md~obsidian/Documents/Your iCloud Vault\"")
return None
return vault_directory
def get_publish_directory():
publish_directory = os.getenv('OBSIDIAN_PICO_PUBLISH_DIRECTORY')
if publish_directory is None:
print("Error: Environment variable OBSIDIAN_PICO_PUBLISH_DIRECTORY is not set.")
print("Please set it using the following command:")
print("export OBSIDIAN_PICO_PUBLISH_DIRECTORY=\"/path/to/your/server/directory\"")
print("Note: We'll add content and images to this directory!")
return None
return publish_directory
def get_publish_url():
publish_url = os.getenv('OBSIDIAN_PICO_PUBLISH_URL')
if publish_url is None:
print("Error: Environment variable OBSIDIAN_PICO_PUBLISH_URL is not set.")
print("Please set it using the following command:")
print("export OBSIDIAN_PICO_PUBLISH_URL=\"https://yoursite.com\"")
return None
return publish_url
def find_md_files_with_slug(directory):
slug_paths = []
for path in Path(directory).rglob('*.md'):
with open(path, 'r') as file:
contents = file.read()
match = re.search(r'---.*?\nobsidian-pico-publish-slug: (.*?)\n.*?---', contents, re.DOTALL)
if match:
slug_paths.append((match.group(1), str(path)))
return slug_paths
def find_images_paths(directory, slug_paths):
# First, get a set of all image references in the markdown files
referenced_images = set()
for _, path in slug_paths:
with open(path, 'r') as file:
contents = file.read()
images = re.findall(r'!\[\]\((.*?)\)', contents)
corrected_images = [img.replace('%20', ' ') for img in images]
referenced_images.update(corrected_images)
# Now, search for each referenced image in the directory tree
image_paths = []
for image in referenced_images:
for root, _, filenames in os.walk(directory):
for filename in fnmatch.filter(filenames, image):
image_paths.append(os.path.join(root, filename))
return image_paths
#####################
# Modify Stuff in Vault to work on Pico
#####################
def update_title(slug_paths):
for _, path in slug_paths:
with open(path, 'r') as file:
contents = file.read()
title = os.path.splitext(os.path.basename(path))[0]
title_pattern = r'\ntitle: .*?\n'
title_replacement = f'\ntitle: {title}\n'
# Check if the title pattern is found in the contents
if re.search(title_pattern, contents, re.DOTALL):
new_contents = re.sub(title_pattern, title_replacement, contents)
else:
new_contents = contents.replace('---', f'---\ntitle: {title}', 1)
# Only write back if the contents have changed
if new_contents != contents:
with open(path, 'w') as file:
file.write(new_contents)
def fix_image_markdown(slug_paths):
for _, path in slug_paths:
with open(path, 'r') as file:
contents = file.read()
# Replace obsidian image link syntax with standard markdown syntax
new_contents = re.sub(r'!\[\[(.*?)\]\]', r'![](\1)', contents)
# Replace spaces in filenames with %20
new_contents = re.sub(r'!\[\]\((.*?)\)', lambda m: '![](' + m.group(1).replace(' ', '%20') + ')', new_contents)
# Only write back if the contents have changed
if new_contents != contents:
with open(path, 'w') as file:
file.write(new_contents)
#####################
# Upload
#####################
def upload_files(host_name, publish_directory, vault_directory, slug_paths, image_paths):
# Upload markdown files
# Extract the paths from slug_paths
all_paths = [os.path.join(vault_directory, path) for _, path in slug_paths]
# Get the list of changed paths
changed_paths = filter_for_changed_paths_and_modify_timestamps_file(vault_directory, all_paths)
# Now filter slug_paths to only keep those whose paths are in changed_paths
slug_paths_skipping_unmodified = [(slug, path) for slug, path in slug_paths if os.path.join(vault_directory, path) in changed_paths]
# Now process these changed_slug_paths
for slug, path in slug_paths_skipping_unmodified:
local_path = os.path.join(vault_directory, path)
new_filename = slug + ".md"
remote_path = f"{host_name}:{os.path.join(publish_directory, 'content', new_filename)}"
os.system(f"scp \"{local_path}\" \"{remote_path}\"")
images_paths_skipping_unmodified = filter_for_changed_paths_and_modify_timestamps_file(vault_directory, image_paths)
# Upload image files
for path in images_paths_skipping_unmodified:
local_path = path
new_filename = os.path.basename(path)
remote_path = f"{host_name}:{os.path.join(publish_directory, 'images', new_filename)}"
os.system(f"scp \"{local_path}\" \"{remote_path}\"")
def filter_for_changed_paths_and_modify_timestamps_file(vault_directory, paths):
obsidian_dir = os.path.join(vault_directory, '.obsidian')
json_path = os.path.join(obsidian_dir, 'obsidian_pico_publish_timestamps.json')
# Load the existing timestamps or initialize an empty dictionary
if os.path.exists(json_path):
with open(json_path, 'r') as json_file:
file_timestamps = json.load(json_file)
else:
file_timestamps = {}
updated_files = []
for path in paths:
short_path = path_without_vault(vault_directory, path)
current_timestamp = os.path.getmtime(path)
# If the path is not in the dictionary or the timestamp has changed
if short_path not in file_timestamps or current_timestamp > file_timestamps[short_path]:
updated_files.append(path)
file_timestamps[short_path] = current_timestamp # Update the timestamp
else:
print(f"Skipping unmodified {short_path}")
# Save the updated timestamps back to the JSON file
with open(json_path, 'w') as json_file:
json.dump(file_timestamps, json_file)
return updated_files
#####################
# Utility
#####################
def path_without_vault(vault_directory, path):
return path.replace(vault_directory, '').lstrip('/')
#####################
# Main
#####################
def main():
host_name = "obsidian-pico-publish-host"
if not check_ssh_host(host_name):
print(f"'{host_name}' is not defined in your SSH config.")
print("Add the following lines to your ~/.ssh/config:")
print(f"""
Host {host_name}
HostName your-host-name
User your-username
""")
return
vault_directory = get_publish_vault_directory()
if vault_directory is None:
return
publish_directory = get_publish_directory()
if publish_directory is None:
return
publish_url = get_publish_url()
if publish_url is None:
return
slug_paths = find_md_files_with_slug(vault_directory)
update_title(slug_paths)
fix_image_markdown(slug_paths)
for slug, path in slug_paths:
print(f"Slug: {slug}, Full Path: {path_without_vault(vault_directory, path)}")
image_paths = find_images_paths(vault_directory,slug_paths)
for image_path in image_paths:
print(f"Image: {path_without_vault(vault_directory, image_path)}")
print("\n\nFiles on host")
for slug, path in slug_paths:
print(f"{publish_url}{slug}")
print(f"\n\nReady to upload files to {host_name}:{publish_directory} to content and images subdirectories")
user_input = input("\n\nPress 'Y' to continue, any other key to quit: ")
if user_input.lower() != 'y':
return
upload_files(host_name, publish_directory, vault_directory, slug_paths, image_paths)
if __name__ == "__main__":
main()
@drosenstark
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment