Skip to content
Permalink
master
Switch branches/tags
Go to file
2 contributors

Users who have contributed to this file

@andymatuschak @markgrovs
# encoding=utf-8
# python3.6
# bear_export_sync.py
# Developed with Visual Studio Code with MS Python Extension.
'''
# Markdown export from Bear sqlite database
Version 1.4, 2020-01-11
modified by: github/andymatuschak, andy_matuschak@twitter
original author: github/rovest, rorves@twitter
See also: bear_import.py for auto import to bear script.
## Sync external updates:
First checks for changes in external Markdown files (previously exported from Bear)
* Replacing text in original note with callback-url replace command
(Keeping original creation date)
If changes in title it will be added just below original title
* New notes are added to Bear (with x-callback-url command)
* New notes get tags from sub folder names, or `#.inbox` if root
* Backing up original note as file to BearSyncBackup folder
(unless a sync conflict, then both notes will be there)
## Export:
Then exporting Markdown from Bear sqlite db.
* check_if_modified() on database.sqlite to see if export is needed
* Uses rsync for copying, so only markdown files of changed sheets will be updated
and synced by Dropbox (or other sync services)
* "Hides" tags with `period+space` on beginning of line: `. #tag` not appear as H1 in other apps.
(This is removed if sync-back above)
* Or instead hide tags in HTML comment blocks like: `<!-- #mytag -->` if `hide_tags_in_comment_block = True`
* Makes subfolders named with first tag in note if `make_tag_folders = True`
* Files can now be copied to multiple tag-folders if `multi_tags = True`
* Export can now be restricted to a list of spesific tags: `limit_export_to_tags = ['bear/github', 'writings']`
or leave list empty for all notes: `limit_export_to_tags = []`
* Can export and link to images in common image repository
* Or export as textbundles with images included
'''
make_tag_folders = True # Exports to folders using first tag only, if `multi_tag_folders = False`
multi_tag_folders = True # Copies notes to all 'tag-paths' found in note!
# Only active if `make_tag_folders = True`
hide_tags_in_comment_block = True # Hide tags in HTML comments: `<!-- #mytag -->`
# The following two lists are more or less mutually exclusive, so use only one of them.
# (You can use both if you have some nested tags where that makes sense)
# Also, they only work if `make_tag_folders = True`.
only_export_these_tags = [] # Leave this list empty for all notes! See below for sample
# only_export_these_tags = ['bear/github', 'writings']
export_as_textbundles = False # Exports as Textbundles with images included
export_as_hybrids = True # Exports as .textbundle only if images included, otherwise as .md
# Only used if `export_as_textbundles = True`
export_image_repository = True # Export all notes as md but link images to
# a common repository exported to: `assets_path`
# Only used if `export_as_textbundles = False`
import os
HOME = os.getenv('HOME', '')
default_out_folder = os.path.join(HOME, "Work", "BearNotes")
default_backup_folder = os.path.join(HOME, "Work", "BearSyncBackup")
# NOTE! Your user 'HOME' path and '/BearNotes' is added below!
# NOTE! So do not change anything below here!!!
import sqlite3
import datetime
import re
import subprocess
import urllib.parse
import time
import shutil
import fnmatch
import json
import argparse
parser = argparse.ArgumentParser(description="Sync Bear notes")
parser.add_argument("--out", default=default_out_folder, help="Path where Bear notes will be synced")
parser.add_argument("--backup", default=default_backup_folder, help="Path where conflicts will be backed up (must be outside of --out)")
parser.add_argument("--images", default=None, help="Path where images will be stored")
parser.add_argument("--skipImport", action="store_const", const=True, default=False, help="When present, the script only exports from Bear to Markdown; it skips the import step.")
parser.add_argument("--excludeTag", action="append", default=[], help="Don't export notes with this tag. Can be used multiple times.")
parsed_args = vars(parser.parse_args())
set_logging_on = True
# NOTE! if 'BearNotes' is left blank, all other files in my_sync_service will be deleted!!
export_path = parsed_args.get("out")
no_export_tags = parsed_args.get("excludeTag") # If a tag in note matches one in this list, it will not be exported.
# NOTE! "export_path" is used for sync-back to Bear, so don't change this variable name!
multi_export = [(export_path, True)] # only one folder output here.
# Use if you want export to severa places like: Dropbox and OneDrive, etc. See below
# Sample for multi folder export:
# export_path_aux1 = os.path.join(HOME, 'OneDrive', 'BearNotes')
# export_path_aux2 = os.path.join(HOME, 'Box', 'BearNotes')
# NOTE! All files in export path not in Bear will be deleted if delete flag is "True"!
# Set this flag fo False only for folders to keep old deleted versions of notes
# multi_export = [(export_path, True), (export_path_aux1, False), (export_path_aux2, True)]
temp_path = os.path.join(HOME, 'Temp', 'BearExportTemp') # NOTE! Do not change the "BearExportTemp" folder name!!!
bear_db = os.path.join(HOME, 'Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite')
sync_backup = parsed_args.get("backup") # Backup of original note before sync to Bear.
log_file = os.path.join(sync_backup, 'bear_export_sync_log.txt')
# Paths used in image exports:
bear_image_path = os.path.join(HOME,
'Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/Local Files/Note Images')
assets_path = parsed_args.get("images") if parsed_args.get("images") else os.path.join(export_path, 'BearImages')
sync_ts = '.sync-time.log'
export_ts = '.export-time.log'
sync_ts_file = os.path.join(export_path, sync_ts)
sync_ts_file_temp = os.path.join(temp_path, sync_ts)
export_ts_file_exp = os.path.join(export_path, export_ts)
export_ts_file = os.path.join(temp_path, export_ts)
gettag_sh = os.path.join(HOME, 'temp/gettag.sh')
gettag_txt = os.path.join(HOME, 'temp/gettag.txt')
def main():
init_gettag_script()
if not parsed_args.get("skipImport"):
sync_md_updates()
if check_db_modified():
delete_old_temp_files()
note_count = export_markdown()
write_time_stamp()
rsync_files_from_temp()
if export_image_repository and not export_as_textbundles:
copy_bear_images()
# notify('Export completed')
write_log(str(note_count) + ' notes exported to: ' + export_path)
exit(1)
else:
print('*** No notes needed exports')
exit(0)
def write_log(message):
if set_logging_on == True:
if not os.path.exists(sync_backup):
os.makedirs(sync_backup)
time_stamp = datetime.datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
message = message.replace(export_path + '/', '')
with open(log_file, 'a', encoding='utf-8') as f:
f.write(time_stamp + ': ' + message + '\n')
def check_db_modified():
if not os.path.exists(sync_ts_file):
return True
db_ts = get_file_date(bear_db)
last_export_ts = get_file_date(export_ts_file_exp)
return db_ts > last_export_ts
def export_markdown():
with sqlite3.connect(bear_db) as conn:
conn.row_factory = sqlite3.Row
query = "SELECT * FROM `ZSFNOTE` WHERE `ZTRASHED` LIKE '0' AND `ZARCHIVED` LIKE '0'"
c = conn.execute(query)
note_count = 0
for row in c:
title = row['ZTITLE']
md_text = row['ZTEXT'].rstrip()
creation_date = row['ZCREATIONDATE']
modified = row['ZMODIFICATIONDATE']
uuid = row['ZUNIQUEIDENTIFIER']
filename = clean_title(title)
file_list = []
if make_tag_folders:
file_list = sub_path_from_tag(temp_path, filename, md_text)
else:
file_list.append(os.path.join(temp_path, filename))
if file_list:
mod_dt = dt_conv(modified)
md_text = hide_tags(md_text)
md_text += '\n\n<!-- {BearID:' + uuid + '} -->\n'
for filepath in file_list:
note_count += 1
# print(filepath)
if export_as_textbundles:
if check_image_hybrid(md_text):
make_text_bundle(md_text, filepath, mod_dt)
else:
write_file(filepath + '.md', md_text, mod_dt)
elif export_image_repository:
md_proc_text = process_image_links(md_text, filepath)
write_file(filepath + '.md', md_proc_text, mod_dt)
else:
write_file(filepath + '.md', md_text, mod_dt)
return note_count
def check_image_hybrid(md_text):
if export_as_hybrids:
if re.search(r'\[image:(.+?)\]', md_text):
return True
else:
return False
else:
return True
def make_text_bundle(md_text, filepath, mod_dt):
'''
Exports as Textbundles with images included
'''
bundle_path = filepath + '.textbundle'
assets_path = os.path.join(bundle_path, 'assets')
if not os.path.exists(bundle_path):
os.makedirs(bundle_path)
os.makedirs(assets_path)
info = '''{
"transient" : true,
"type" : "net.daringfireball.markdown",
"creatorIdentifier" : "net.shinyfrog.bear",
"version" : 2
}'''
matches = re.findall(r'\[image:(.+?)\]', md_text)
for match in matches:
image_name = match
new_name = image_name.replace('/', '_')
source = os.path.join(bear_image_path, image_name)
target = os.path.join(assets_path, new_name)
shutil.copy2(source, target)
md_text = re.sub(r'\[image:(.+?)/(.+?)\]', r'![](assets/\1_\2)', md_text)
write_file(bundle_path + '/text.md', md_text, mod_dt)
write_file(bundle_path + '/info.json', info, mod_dt)
os.utime(bundle_path, (-1, mod_dt))
def sub_path_from_tag(temp_path, filename, md_text):
# Get tags in note:
pattern1 = r'(?<!\S)\#([.\w\/\-]+)[ \n]?(?!([\/ \w]+\w[#]))'
pattern2 = r'(?<![\S])\#([^ \d][.\w\/ ]+?)\#([ \n]|$)'
if multi_tag_folders:
# Files copied to all tag-folders found in note
tags = []
for matches in re.findall(pattern1, md_text):
tag = matches[0]
tags.append(tag)
for matches2 in re.findall(pattern2, md_text):
tag2 = matches2[0]
tags.append(tag2)
if len(tags) == 0:
# No tags found, copy to root level only
return [os.path.join(temp_path, filename)]
else:
# Only folder for first tag
match1 = re.search(pattern1, md_text)
match2 = re.search(pattern2, md_text)
if match1 and match2:
if match1.start(1) < match2.start(1):
tag = match1.group(1)
else:
tag = match2.group(1)
elif match1:
tag = match1.group(1)
elif match2:
tag = match2.group(1)
else:
# No tags found, copy to root level only
return [os.path.join(temp_path, filename)]
tags = [tag]
paths = [os.path.join(temp_path, filename)]
for tag in tags:
if tag == '/':
continue
if only_export_these_tags:
export = False
for export_tag in only_export_these_tags:
if tag.lower().startswith(export_tag.lower()):
export = True
break
if not export:
continue
for no_tag in no_export_tags:
if tag.lower().startswith(no_tag.lower()):
return []
if tag.startswith('.'):
# Avoid hidden path if it starts with a '.'
sub_path = '_' + tag[1:]
else:
sub_path = tag
tag_path = os.path.join(temp_path, sub_path)
if not os.path.exists(tag_path):
os.makedirs(tag_path)
paths.append(os.path.join(tag_path, filename))
return paths
def process_image_links(md_text, filepath):
'''
Bear image links converted to MD links
'''
root = filepath.replace(temp_path, '')
level = len(root.split('/')) - 2
parent = '../' * level
md_text = re.sub(r'\[image:(.+?)\]', r'![](' + parent + r'BearImages/\1)', md_text)
return md_text
def restore_image_links(md_text):
'''
MD image links restored back to Bear links
'''
#if not re.search(r'!\[.*?\]\(assets/.+?\)', md_text):
# No image links in note, return unchanged:
# return md_text
if export_as_textbundles:
md_text = re.sub(r'!\[(.*?)\]\(assets/(.+?)_(.+?)( ".+?")?\) ?', r'[image:\2/\3]\4 \1', md_text)
elif export_image_repository :
# md_text = re.sub(r'\[image:(.+?)\]', r'![](../assets/\1)', md_text)
md_text = re.sub(r'!\[\]\((\.\./)*BearImages/(.+?)\)', r'[image:\2]', md_text)
return md_text
def copy_bear_images():
# Image files copied to a common image repository
subprocess.call(['rsync', '-r', '-t', '--delete',
bear_image_path + "/", assets_path])
def write_time_stamp():
# write to time-stamp.txt file (used during sync)
write_file(export_ts_file, "Markdown from Bear written at: " +
datetime.datetime.now().strftime("%Y-%m-%d at %H:%M:%S"), 0)
write_file(sync_ts_file_temp, "Markdown from Bear written at: " +
datetime.datetime.now().strftime("%Y-%m-%d at %H:%M:%S"), 0)
def hide_tags(md_text):
# Hide tags from being seen as H1, by placing `period+space` at start of line:
if hide_tags_in_comment_block:
md_text = re.sub(r'(\n)[ \t]*(\#[^\s#].*)', r'\1<!-- \2 -->', md_text)
else:
md_text = re.sub(r'(\n)[ \t]*(\#[^\s#]+)', r'\1. \2', md_text)
return md_text
def restore_tags(md_text):
# Tags back to normal Bear tags, stripping the `period+space` at start of line:
# if hide_tags_in_comment_block:
md_text = re.sub(r'(\n)<!--[ \t]*(\#[^\s#].*?) -->', r'\1\2', md_text)
# else:
md_text = re.sub(r'(\n)\.[ \t]*(\#[^\s#]+)', r'\1\2', md_text)
return md_text
def clean_title(title):
title = title[:256].strip()
if title == "":
title = "Untitled"
title = re.sub(r'[/\\*?$@!^&\|~:\.]', r'-', title)
title = re.sub(r'-$', r'', title)
return title.strip()
def write_file(filename, file_content, modified):
with open(filename, "w", encoding='utf-8') as f:
f.write(file_content)
if modified > 0:
os.utime(filename, (-1, modified))
def read_file(file_name):
with open(file_name, "r", encoding='utf-8') as f:
file_content = f.read()
return file_content
def get_file_date(filename):
try:
t = os.path.getmtime(filename)
return t
except:
return 0
def dt_conv(dtnum):
# Formula for date offset based on trial and error:
hour = 3600 # seconds
year = 365.25 * 24 * hour
offset = year * 31 + hour * 6
return dtnum + offset
def date_time_conv(dtnum):
newnum = dt_conv(dtnum)
dtdate = datetime.datetime.fromtimestamp(newnum)
#print(newnum, dtdate)
return dtdate.strftime(' - %Y-%m-%d_%H%M')
def time_stamp_ts(ts):
dtdate = datetime.datetime.fromtimestamp(ts)
return dtdate.strftime('%Y-%m-%d at %H:%M')
def date_conv(dtnum):
dtdate = datetime.datetime.fromtimestamp(dtnum)
return dtdate.strftime('%Y-%m-%d')
def delete_old_temp_files():
# Deletes all files in temp folder before new export using "shutil.rmtree()":
# NOTE! CAUTION! Do not change this function unless you really know shutil.rmtree() well!
if os.path.exists(temp_path) and "BearExportTemp" in temp_path:
# *** NOTE! Double checking that temp_path folder actually contains "BearExportTemp"
# *** Because if temp_path is accidentally empty or root,
# *** shutil.rmtree() will delete all files on your complete Hard Drive ;(
shutil.rmtree(temp_path)
# *** NOTE: USE rmtree() WITH EXTREME CAUTION!
os.makedirs(temp_path)
def rsync_files_from_temp():
# Moves markdown files to new folder using rsync:
# This is a very important step!
# By first exporting all Bear notes to an emptied temp folder,
# rsync will only update destination if modified or size have changed.
# So only changed notes will be synced by Dropbox or OneDrive destinations.
# Rsync will also delete notes on destination if deleted in Bear.
# So doing it this way saves a lot of otherwise very complex programing.
# Thank you very much, Rsync! ;)
for (dest_path, delete) in multi_export:
if not os.path.exists(dest_path):
os.makedirs(dest_path)
if delete:
subprocess.call(['rsync', '-r', '-t', '-E', '--delete',
'--exclude', 'BearImages/',
'--exclude', '.Ulysses*',
'--exclude', '*.Ulysses_Public_Filter',
temp_path + "/", dest_path])
else:
subprocess.call(['rsync', '-r', '-t', '-E',
temp_path + "/", dest_path])
def sync_md_updates():
updates_found = False
if not os.path.exists(sync_ts_file) or not os.path.exists(export_ts_file):
return False
ts_last_sync = os.path.getmtime(sync_ts_file)
ts_last_export = os.path.getmtime(export_ts_file)
# Update synced timestamp file:
update_sync_time_file(0)
file_types = ('*.md', '*.txt', '*.markdown')
for (root, dirnames, filenames) in os.walk(export_path):
'''
This step walks down into all sub folders, if any.
'''
for pattern in file_types:
for filename in fnmatch.filter(filenames, pattern):
md_file = os.path.join(root, filename)
ts = os.path.getmtime(md_file)
if ts > ts_last_sync:
if not updates_found: # Yet
# Wait 5 sec at first for external files to finish downloading from dropbox.
# Otherwise images in textbundles might be missing in import:
time.sleep(5)
updates_found = True
md_text = read_file(md_file)
backup_ext_note(md_file)
if check_if_image_added(md_text, md_file):
textbundle_to_bear(md_text, md_file, ts)
write_log('Imported to Bear: ' + md_file)
else:
update_bear_note(md_text, md_file, ts, ts_last_export)
write_log('Bear Note Updated: ' + md_file)
if updates_found:
# Give Bear time to process updates:
time.sleep(3)
# Check again, just in case new updates synced from remote (OneDrive/Dropbox)
# during this process!
# The logic is not 100% fool proof, but should be close to 99.99%
sync_md_updates() # Recursive call
return updates_found
def check_if_image_added(md_text, md_file):
if not '.textbundle/' in md_file:
return False
matches = re.findall(r'!\[.*?\]\(assets/(.+?_).+?\)', md_text)
for image_match in matches:
'F89CDA3D-3FCC-4E92-88C1-CC4AF46FA733-10097-00002BBE9F7FF804_IMG_2280.JPG'
if not re.match(r'[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}-[0-9A-F]{3,5}-[0-9A-F]{16}_', image_match):
return True
return False
def textbundle_to_bear(md_text, md_file, mod_dt):
md_text = restore_tags(md_text)
bundle = os.path.split(md_file)[0]
match = re.search(r'\{BearID:(.+?)\}', md_text)
if match:
uuid = match.group(1)
# Remove old BearID: from new note
md_text = re.sub(r'\<\!-- ?\{BearID\:' + uuid + r'\} ?--\>', '', md_text).rstrip() + '\n'
md_text = insert_link_top_note(md_text, 'Images added! Link to original note: ', uuid)
else:
# New textbundle (with images), add path as tag:
md_text = get_tag_from_path(md_text, bundle, export_path)
write_file(md_file, md_text, mod_dt)
os.utime(bundle, (-1, mod_dt))
subprocess.call(['open', '-a', '/applications/bear.app', bundle])
time.sleep(0.5)
def backup_ext_note(md_file):
if '.textbundle' in md_file:
bundle_path = os.path.split(md_file)[0]
bundle_name = os.path.split(bundle_path)[1]
target = os.path.join(sync_backup, bundle_name)
bundle_raw = os.path.splitext(target)[0]
count = 2
while os.path.exists(target):
# Adding sequence number to identical filenames, preventing overwrite:
target = bundle_raw + " - " + str(count).zfill(2) + ".textbundle"
count += 1
shutil.copytree(bundle_path, target)
else:
# Overwrite former bacups of incoming changes, only keeps last one:
shutil.copy2(md_file, sync_backup + '/')
def update_sync_time_file(ts):
write_file(sync_ts_file,
"Checked for Markdown updates to sync at: " +
datetime.datetime.now().strftime("%Y-%m-%d at %H:%M:%S"), ts)
def update_bear_note(md_text, md_file, ts, ts_last_export):
md_text = restore_tags(md_text)
md_text = restore_image_links(md_text)
uuid = ''
match = re.search(r'\{BearID:(.+?)\}', md_text)
sync_conflict = False
if match:
uuid = match.group(1)
# Remove old BearID: from new note
md_text = re.sub(r'\<\!-- ?\{BearID\:' + uuid + r'\} ?--\>', '', md_text).rstrip() + '\n'
sync_conflict = check_sync_conflict(uuid, ts_last_export)
if sync_conflict:
link_original = 'bear://x-callback-url/open-note?id=' + uuid
message = '::Sync conflict! External update: ' + time_stamp_ts(ts) + '::'
message += '\n[Click here to see original Bear note](' + link_original + ')'
x_create = 'bear://x-callback-url/create?show_window=no&open_note=no'
bear_x_callback(x_create, md_text, message, '')
else:
# Regular external update
orig_title = backup_bear_note(uuid)
# message = '::External update: ' + time_stamp_ts(ts) + '::'
x_replace = 'bear://x-callback-url/add-text?show_window=no&open_note=no&mode=replace&id=' + uuid
bear_x_callback(x_replace, md_text, '', orig_title)
# # Trash old original note:
# x_trash = 'bear://x-callback-url/trash?show_window=no&id=' + uuid
# subprocess.call(["open", x_trash])
# time.sleep(.2)
else:
# New external md Note, since no Bear uuid found in text:
# message = '::New external Note - ' + time_stamp_ts(ts) + '::'
md_text = get_tag_from_path(md_text, md_file, export_path)
x_create = 'bear://x-callback-url/create?show_window=no'
bear_x_callback(x_create, md_text, '', '')
return
def get_tag_from_path(md_text, md_file, root_path, inbox_for_root=True, extra_tag=''):
# extra_tag should be passed as '#tag' or '#space tag#'
path = md_file.replace(root_path, '')[1:]
sub_path = os.path.split(path)[0]
tags = []
if '.textbundle' in sub_path:
sub_path = os.path.split(sub_path)[0]
if sub_path == '':
if inbox_for_root:
tag = '#.inbox'
else:
tag = ''
elif sub_path.startswith('_'):
tag = '#.' + sub_path[1:].strip()
else:
tag = '#' + sub_path.strip()
if ' ' in tag:
tag += "#"
if tag != '':
tags.append(tag)
if extra_tag != '':
tags.append(extra_tag)
for tag in get_file_tags(md_file):
tag = '#' + tag.strip()
if ' ' in tag: tag += "#"
tags.append(tag)
return md_text.strip() + '\n\n' + ' '.join(tags) + '\n'
def get_file_tags(md_file):
try:
subprocess.call([gettag_sh, md_file, gettag_txt])
text = re.sub(r'\\n\d{1,2}', r'', read_file(gettag_txt))
tag_list = json.loads(text)
return tag_list
except:
return []
def bear_x_callback(x_command, md_text, message, orig_title):
if message != '':
lines = md_text.splitlines()
lines.insert(1, message)
md_text = '\n'.join(lines)
if orig_title != '':
lines = md_text.splitlines()
title = re.sub(r'^#+ ', r'', lines[0])
if title != orig_title:
md_text = '\n'.join(lines)
else:
md_text = '\n'.join(lines[1:])
x_command_text = x_command + '&text=' + urllib.parse.quote(md_text)
subprocess.call(["open", "-g", x_command_text])
time.sleep(.2)
def check_sync_conflict(uuid, ts_last_export):
conflict = False
# Check modified date of original note in Bear sqlite db!
with sqlite3.connect(bear_db) as conn:
conn.row_factory = sqlite3.Row
query = "SELECT * FROM `ZSFNOTE` WHERE `ZTRASHED` LIKE '0' AND `ZUNIQUEIDENTIFIER` LIKE '" + uuid + "'"
c = conn.execute(query)
for row in c:
modified = row['ZMODIFICATIONDATE']
uuid = row['ZUNIQUEIDENTIFIER']
mod_dt = dt_conv(modified)
conflict = mod_dt > ts_last_export
return conflict
def backup_bear_note(uuid):
# Get single note from Bear sqlite db!
with sqlite3.connect(bear_db) as conn:
conn.row_factory = sqlite3.Row
query = "SELECT * FROM `ZSFNOTE` WHERE `ZUNIQUEIDENTIFIER` LIKE '" + uuid + "'"
c = conn.execute(query)
title = ''
for row in c: # Will only get one row if uuid is found!
title = row['ZTITLE']
md_text = row['ZTEXT'].rstrip()
modified = row['ZMODIFICATIONDATE']
mod_dt = dt_conv(modified)
created = row['ZCREATIONDATE']
cre_dt = dt_conv(created)
md_text = insert_link_top_note(md_text, 'Link to updated note: ', uuid)
dtdate = datetime.datetime.fromtimestamp(cre_dt)
filename = clean_title(title) + dtdate.strftime(' - %Y-%m-%d_%H%M')
if not os.path.exists(sync_backup):
os.makedirs(sync_backup)
file_part = os.path.join(sync_backup, filename)
# This is a Bear text file, not exactly markdown.
backup_file = file_part + ".txt"
count = 2
while os.path.exists(backup_file):
# Adding sequence number to identical filenames, preventing overwrite:
backup_file = file_part + " - " + str(count).zfill(2) + ".txt"
count += 1
write_file(backup_file, md_text, mod_dt)
filename2 = os.path.split(backup_file)[1]
write_log('Original to sync_backup: ' + filename2)
return title
def insert_link_top_note(md_text, message, uuid):
lines = md_text.split('\n')
title = re.sub(r'^#{1,6} ', r'', lines[0])
link = '::' + message + '[' + title + '](bear://x-callback-url/open-note?id=' + uuid + ')::'
lines.insert(1, link)
return '\n'.join(lines)
def init_gettag_script():
gettag_script = \
'''#!/bin/bash
if [[ ! -e $1 ]] ; then
echo 'file missing or not specified'
exit 0
fi
JSON="$(xattr -p com.apple.metadata:_kMDItemUserTags "$1" | xxd -r -p | plutil -convert json - -o -)"
echo $JSON > "$2"
'''
temp = os.path.join(HOME, 'temp')
if not os.path.exists(temp):
os.makedirs(temp)
write_file(gettag_sh, gettag_script, 0)
subprocess.call(['chmod', '777', gettag_sh])
def notify(message):
title = "ul_sync_md.py"
try:
# Uses "terminal-notifier", download at:
# https://github.com/julienXX/terminal-notifier/releases/download/2.0.0/terminal-notifier-2.0.0.zip
# Only works with MacOS 10.11+
subprocess.call(['/Applications/terminal-notifier.app/Contents/MacOS/terminal-notifier',
'-message', message, "-title", title, '-sound', 'default'])
except:
write_log('"terminal-notifier.app" is missing!')
return
if __name__ == '__main__':
main()