Last active
March 25, 2019 10:54
-
-
Save e-roux/f09e56c6f1d15c3f4704fecaf4b8ff33 to your computer and use it in GitHub Desktop.
Mac OS: python module for easily adding, removing, and moving positions of Safari bookmarks in the context of the currently logged in user.
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/python | |
# | |
# From https://github.com/robperc/SafariBookmarkEditor | |
# | |
# Revision | |
# 23/10/2017 - ported to Python 3, PEP8 fixes | |
# | |
""" | |
Python module for easily adding, removing, and moving positions of Safari | |
bookmarks in the context of the currently logged in user. | |
On fresh installs that lack a Bookmarks.plist in the ~/Library/Safari/ folder or | |
in the case of a corrupt plist a boilerplate Bookmarks plist will be generated | |
with the proper format. Bookmark titles are checked against existing bookmarks | |
to ensure no collisions occur. Can also be run as a CLI tool. | |
""" | |
import argparse | |
import os | |
import plistlib | |
import subprocess | |
import uuid | |
def touch(fname, mode=0o666, dir_fd=None, **kwargs): | |
""" | |
Implementation of Unix utility | |
https://stackoverflow.com/questions/1158076 | |
""" | |
flags = os.O_CREAT | os.O_APPEND | |
with os.fdopen(os.open(fname, flags=flags, | |
mode=mode, dir_fd=dir_fd)) as _file: | |
os.utime(_file.fileno() if os.utime in os.supports_fd else fname, | |
dir_fd=None if os.supports_fd else dir_fd, **kwargs) | |
class SafariBookmarks(object): | |
""" | |
Creates a Safari bookmark instance in the context of the current user. | |
Attributes | |
---------- | |
pathtoplist (str) | |
Path to Bookmarks plist of current user. | |
plist (dict(dict, ..., dict)): | |
XML of bookmarks dictionary parsed into plist. | |
titles (list(str, ..., str)) | |
List of titles of current bookmarks. | |
bookmarks (list(dict, ..., dict)) | |
Reference to bookmarks entry of parsed plist dictionary. | |
""" | |
def __init__(self): | |
self.plist = dict() | |
self.titles = list() | |
self.bookmarks = list() | |
self.read() | |
@property | |
def pathtoplist(self): | |
""" | |
Check to see Bookmarks plist exists and has correct form. | |
If either of these conditions aren't met replaces existing | |
plist with new empty one. | |
Returns | |
------- | |
Expanded path to ~/Library/Safari/Bookmarks.plist | |
""" | |
_path = os.path.expanduser('~/Library/Safari/Bookmarks.plist') | |
if not os.path.isfile(_path): | |
print("Bookmarks.plist doesn't appear to exist." | |
"Generating new Bookmarks.plist.") | |
self.generate() | |
return _path | |
@staticmethod | |
def generate(): | |
""" | |
Generate a boilerplate Safari Bookmarks plist at plist path. | |
Raises | |
------ | |
CalledProcessError if creation of plist fails. | |
""" | |
pathtoplist = os.path.expanduser('~/Library/Safari/Bookmarks.plist') | |
touch(pathtoplist) | |
contents = dict( | |
Children=list(( | |
dict( | |
Title="History", | |
WebBookmarkIdentifier="History", | |
WebBookmarkType="WebBookmarkTypeProxy", | |
WebBookmarkUUID=str(uuid.uuid5(uuid.NAMESPACE_DNS, | |
"History")), | |
), | |
dict( | |
Children=list(), | |
Title="BookmarksBar", | |
WebBookmarkType="WebBookmarkTypeList", | |
WebBookmarkUUID=str(uuid.uuid5(uuid.NAMESPACE_DNS, | |
"BookmarksBar")), | |
), | |
dict( | |
Title="BookmarksMenu", | |
WebBookmarkType="WebBookmarkTypeList", | |
WebBookmarkUUID=str(uuid.uuid5(uuid.NAMESPACE_DNS, | |
"BookmarksMenu")), | |
), | |
)), | |
Title="", | |
WebBookmarkFileVersion=1, | |
WebBookmarkType="WebBookmarkTypeList", | |
WebBookmarkUUID=str(uuid.uuid5(uuid.NAMESPACE_DNS, "")), | |
) | |
plistlib.dump(contents, pathtoplist) | |
def read(self): | |
""" | |
Parse plist into dictionary. Creates a new empty bookmarks plist | |
if plist can't be read. | |
Returns | |
------- | |
Dictionary containing info parsed from bookmarks plist. | |
""" | |
subprocess.call(['plutil', '-convert', 'xml1', self.pathtoplist]) | |
try: | |
plist = plistlib.load(self.pathtoplist) | |
except Exception as _ex: | |
# handle any other exception | |
print("Error '{_ex.message}' occured. Arguments {_ex.args}." | |
"Bookmarks.plist appears to be corrupted." | |
"Generating new Bookmarks.plist.") | |
self.generate() # if plist can't be read generate new empty one. | |
with open(self.pathtoplist, 'rb') as _file: | |
plist = plistlib.load(_file) | |
self.plist = plist | |
self.bookmarks = self.plist['Children'][1]['Children'] | |
self.titles = [bm["URIDictionary"]["title"] for bm in self.bookmarks | |
if bm.get("URIDictionary") is not None] | |
def add(self, title, url, index=-1): | |
""" | |
Add a bookmark to plist dictionary. | |
Parameters | |
---------- | |
title (str) | |
Title to label bookmark with. | |
url (str) | |
Url to bookmark. | |
""" | |
if title in self.titles: | |
print(f"Warning: Found preexisting bookmark with " | |
f"title {title}, skipping.") | |
return | |
if index == -1 or index > len(self.bookmarks): | |
index = len(self.bookmarks) | |
elif index < -1: | |
index = 0 | |
bookmark = dict( | |
WebBookmarkType='WebBookmarkTypeLeaf', | |
WebBookmarkUUID=str(uuid.uuid5(uuid.NAMESPACE_DNS, title)), | |
URLString=url, | |
URIDictionary=dict( | |
title=title | |
), | |
) | |
self.bookmarks.insert(index, bookmark) | |
self.titles.append(title) | |
def remove(self, title): | |
""" | |
Removes bookmark identified by title from plist dictionary if found. | |
Parameters | |
---------- | |
title (str): Title bookmark is identified by. | |
""" | |
if title not in self.titles: | |
return | |
for bookmark in self.bookmarks: | |
if bookmark.get("URIDictionary") and \ | |
bookmark["URIDictionary"]["title"] == title: | |
self.titles.remove(title) | |
self.bookmarks.remove(bookmark) | |
return | |
def remove_all(self): | |
""" | |
Removes all bookmarks from the plist dictionary. | |
""" | |
# Remove bookmarks in reveresed order to avoid shifting issues | |
for bookmark in reversed(self.bookmarks): | |
self.bookmarks.remove(bookmark) | |
self.titles = list() | |
def move(self, title, index): | |
""" | |
Move bookmark identified by title to specified index | |
in order of bookmarks if found. | |
Parameters | |
---------- | |
title (str): Title bookmark is identified by. | |
index (int): Index to move bookmark to. | |
""" | |
if title not in self.titles: | |
return | |
if index > len(self.bookmarks) or index == -1: | |
index = len(self.bookmarks) | |
elif index < -1: | |
index = 0 | |
for bookmark in self.bookmarks: | |
if bookmark.get('URIDictionary') and \ | |
bookmark['URIDictionary']['title'] == title: | |
to_mv = bookmark | |
break | |
self.bookmarks.remove(to_mv) | |
self.bookmarks.insert(index, to_mv) | |
def swap(self, title1, title2): | |
""" | |
Swap position of bookmark identified by title1 with bookmark | |
identified by title2 if both are found. | |
Parameters | |
---------- | |
title1 (str): Title 1st bookmark is identified by. | |
title2 (str): Title 2nd bookmark is identified by. | |
""" | |
if (title1 not in self.titles) or (title2 not in self.titles) \ | |
or (title1 == title2): | |
return | |
for index, bookmark in enumerate(self.bookmarks): | |
if bookmark.get('URIDictionary') and \ | |
bookmark['URIDictionary']['title'] == title1: | |
index1 = index | |
bookmark1 = bookmark | |
if bookmark.get('URIDictionary') and \ | |
bookmark['URIDictionary']['title'] == title2: | |
index2 = index | |
bookmark2 = bookmark | |
self.bookmarks[index1] = bookmark2 | |
self.bookmarks[index2] = bookmark1 | |
def write(self): | |
""" | |
Writes modified plist dictionary to bookmarks plist and | |
converts to binary format. | |
""" | |
plistlib.dump(self.plist, self.pathtoplist) | |
subprocess.call(['plutil', '-convert', 'binary1', self.pathtoplist]) | |
def get_index(self, title): | |
""" | |
Return index of bookmark whose title is in URIDictionary. | |
""" | |
if title not in self.titles: | |
return None | |
for index, _bm in enumerate(self.bookmarks): | |
if _bm.get("URIDictionary") is None: | |
continue | |
bm_title = _bm["URIDictionary"]["title"] | |
if bm_title == title: | |
return index | |
def main(): | |
""" | |
main function. | |
""" | |
parser = argparse.ArgumentParser(description='Command line tool for adding ' | |
'and removing Safari bookmarks in the ' | |
'context of the currently logged in user.') | |
parser.add_argument('--add', metavar='title::url', type=str, nargs='+', | |
help='double-colon separated title and bookmark(s) ' | |
'url(s) to add in, e.g.: ' | |
'--add myWebsite::http://www.mywebsite.com ' | |
' MyOtherWebsite::http://www.myotherwebsite.com') | |
parser.add_argument('--remove', metavar='title', type=str, nargs='+', | |
help='title(s) of bookmark(s) to remove, e..: ' | |
'--remove MyWebsite MyOtherWebsite') | |
parser.add_argument('--removeall', action='store_true', | |
help='remove all current bookmarks') | |
args = parser.parse_args() | |
bookmarks = SafariBookmarks() | |
if args.removeall: | |
bookmarks.remove_all() | |
if args.remove: | |
for title in args.remove: | |
bookmarks.remove(title) | |
if args.add: | |
for bookmark in args.add: | |
title, url = bookmark.split('::') | |
bookmarks.add(title, url) | |
bookmarks.write() | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment