Created
August 7, 2023 15:46
-
-
Save drosenstark/7bb1a6a84c3185614ecbf5c96dd26e73 to your computer and use it in GitHub Desktop.
Obsidian Pico Publish, for Publishing Obsidian Notes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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'', contents) | |
# Replace spaces in filenames with %20 | |
new_contents = re.sub(r'!\[\]\((.*?)\)', lambda m: '.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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See
https://pages.dr2050.com/?obsidian-pico-publish-readme