I don't know much about alfred, and changing alfred.py doesn't seem to be the right way(though it works with python3)
chrome.py
#!/usr/bin/env python3
"""
Get relevant history from the Google Chrome history database based on the given query and build
Alfred items based on the results.
Usage:
chrome.py [--no-favicons | --favicons] PROFILE QUERY
chrome.py (-h | --help)
chrome.py --version
The path to the Chrome user profile to get the history database from is given in PROFILE. The query
to search for is given in QUERY. The output is formatted as the Alfred script filter XML output
format.
PROFILE The path to the Chrome profile whose history database should be searched
QUERY The query to search the history database with
Options:
--no-favicons Do not return Alfred XML results with favicons [default: false]
--favicons Include favicons in the Alfred XML results [default: true]
"""
import alfred
import sqlite3
import shutil
import os
import sys
import time
import datetime
from docopt import docopt
__version__ = '0.7.0'
CACHE_EXPIRY = 60
HISTORY_DB = 'History'
FAVICONS_DB = 'Favicons'
FAVICONS_CACHE = 'Favicons-Cache'
FAVICON_JOIN = """
LEFT OUTER JOIN icon_mapping ON icon_mapping.page_url = urls.url,
favicon_bitmaps ON favicon_bitmaps.id =
(SELECT id FROM favicon_bitmaps
WHERE favicon_bitmaps.icon_id = icon_mapping.icon_id
ORDER BY width DESC LIMIT 1)
"""
FAVICON_SELECT = """
, favicon_bitmaps.image_data, favicon_bitmaps.last_updated
"""
HISTORY_QUERY = """
SELECT urls.id, urls.title, urls.url {favicon_select}
FROM urls
{favicon_join}
WHERE (urls.title LIKE ? OR urls.url LIKE ?)
ORDER BY visit_count DESC, typed_count DESC, last_visit_time DESC
"""
# UNIX_EPOCH = datetime.datetime.fromtimestamp(0, datetime.timezone.utc)
# WINDOWS_EPOCH = datetime.datetime(1601, 1, 1)
# SECONDS_BETWEEN_UNIX_AND_WINDOWS_EPOCH = (UNIX_EPOCH - WINDOWS_EPOCH).total_seconds()
UNIX_EPOCH = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
WINDOWS_EPOCH = datetime.datetime(1601, 1, 1, tzinfo=datetime.timezone.utc)
SECONDS_BETWEEN_UNIX_AND_WINDOWS_EPOCH = (UNIX_EPOCH - WINDOWS_EPOCH).total_seconds()
MICROSECS_PER_SEC = 10 ** -6
class ErrorItem(alfred.Item):
def __init__(self, error):
super().__init__({u'valid': u'NO', u'autocomplete': error}, error, u'Check the workflow log for more information.')
def alfred_error(error):
alfred.write(alfred.xml([ErrorItem(error)]))
def copy_db(name, profile):
cache = os.path.join(alfred.work(True), name)
if os.path.isfile(cache) and time.time() - os.path.getmtime(cache) < CACHE_EXPIRY:
return cache
db_file = os.path.join(os.path.expanduser(profile), name)
try:
shutil.copy(db_file, cache)
except:
raise IOError('Unable to copy Google Chrome history database from {}'.format(db_file))
return cache
def history_db(profile, favicons=True):
history = copy_db(HISTORY_DB, profile)
db = sqlite3.connect(history)
if favicons:
favicons = copy_db(FAVICONS_DB, profile)
db.cursor().execute('ATTACH DATABASE ? AS favicons', (favicons,)).close()
return db
def cache_favicon(image_data, uid, last_updated):
cache_dir = os.path.join(alfred.work(True), FAVICONS_CACHE)
if not os.path.isdir(cache_dir):
os.makedirs(cache_dir)
icon_file = os.path.join(cache_dir, str(uid))
# Convert last_updated to timestamp for comparison
last_updated_ts = last_updated.timestamp()
if not os.path.isfile(icon_file) or last_updated_ts > os.path.getmtime(icon_file):
with open(icon_file, 'wb') as f: # Ensure binary write mode
f.write(image_data)
# Update the file's modification time to last_updated_ts
os.utime(icon_file, (time.time(), last_updated_ts))
return (icon_file, {'type': 'png'})
def convert_chrometime(chrometime):
return datetime.datetime.fromtimestamp((chrometime / 10**6) - SECONDS_BETWEEN_UNIX_AND_WINDOWS_EPOCH, tz=datetime.timezone.utc)
def history_results(db, query, favicons=True):
q = u'%{}%'.format(query)
if favicons:
favicon_select = FAVICON_SELECT
favicon_join = FAVICON_JOIN
else:
favicon_select = ''
favicon_join = ''
for row in db.execute(HISTORY_QUERY.format(favicon_select=favicon_select, favicon_join=favicon_join), (q, q)):
if favicons:
(uid, title, url, image_data, image_last_updated) = row
icon = cache_favicon(image_data, uid, convert_chrometime(image_last_updated)) if image_data and image_last_updated else None
else:
(uid, title, url) = row
icon = None
yield alfred.Item({u'uid': alfred.uid(uid), u'arg': url, u'autocomplete': url}, title or url, url, icon=icon)
if __name__ == '__main__':
arguments = docopt(__doc__, version=__version__)
favicons = arguments.get('--no-favicons') is False
profile = arguments.get('PROFILE')
query = arguments.get('QUERY')
try:
db = history_db(profile, favicons=favicons)
except IOError as e:
alfred_error(e)
sys.exit(-1)
alfred.write(alfred.xml(list(history_results(db, query, favicons=favicons))))
alfred.py
import itertools
import os
import plistlib
import unicodedata
import sys
from xml.etree.ElementTree import Element, SubElement, tostring
UNESCAPE_CHARACTERS = " ;()"
_MAX_RESULTS_DEFAULT = 9
# Updated to use plistlib.load for Python 3 compatibility
with open('info.plist', 'rb') as fp:
preferences = plistlib.load(fp)
bundleid = preferences['bundleid']
class Item:
@classmethod
def unicode(cls, value):
try:
items = iter(value.items())
except AttributeError:
# Directly return the value if it's not a dict
return str(value)
else:
# Use str instead of unicode for Python 3 compatibility
return dict(map(cls.unicode, item) for item in items)
def __init__(self, attributes, title, subtitle, icon=None):
self.attributes = attributes
self.title = title
self.subtitle = subtitle
self.icon = icon
def __str__(self):
return tostring(self.xml()).decode('utf-8')
def xml(self):
item = Element('item', self.unicode(self.attributes))
for attribute in ('title', 'subtitle', 'icon'):
value = getattr(self, attribute)
if value is None:
continue
if len(value) == 2 and isinstance(value[1], dict):
(value, attributes) = value
else:
attributes = {}
SubElement(item, attribute, self.unicode(attributes)).text = self.unicode(value)
return item
def args(characters=None):
return tuple(unescape(decode(arg), characters) for arg in sys.argv[1:])
def config():
return _create('config')
def decode(s):
# Removed .decode('utf-8') as strings are already Unicode in Python 3
return unicodedata.normalize('NFD', s)
def env(key):
return os.environ[f'alfred_{key}']
def uid(uid):
return '-'.join(map(str, (bundleid, uid)))
def unescape(query, characters=None):
for character in (UNESCAPE_CHARACTERS if (characters is None) else characters):
query = query.replace(f'\\{character}', character)
return query
def work(volatile):
path = {
True: env('workflow_cache'),
False: env('workflow_data')
}[bool(volatile)]
return _create(path)
def write(text):
if isinstance(text, bytes):
text = text.decode('utf-8')
sys.stdout.write(text)
def xml(items, maxresults=_MAX_RESULTS_DEFAULT):
root = Element('items')
for item in itertools.islice(items, maxresults):
root.append(item.xml())
return tostring(root, encoding='utf-8')
def _create(path):
if not os.path.isdir(path):
os.mkdir(path)
if not os.access(path, os.W_OK):
raise IOError(f'No write access: {path}')
return path
Current version supports python below version 3. I'd like to share the updated version google_chrome_history_workflow_python3.zip
What has changed
I don't know much about alfred, and changing alfred.py doesn't seem to be the right way(though it works with python3)
chrome.py
alfred.py