Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"Name": "Browser History",
"Description": "Search your Web Browser history",
"Author": "Garulf",
"Version": "0.2.0",
"Version": "1.0.0",
"Language": "python",
"Website": "https://github.com/Garulf/browser-history",
"IcoPath": "./icon.png",
Expand Down
183 changes: 57 additions & 126 deletions plugin/browsers.py
Original file line number Diff line number Diff line change
@@ -1,150 +1,81 @@
import shutil
import sqlite3
from tempfile import gettempdir
from tempfile import mktemp
import os
from pathlib import Path
from datetime import datetime
import logging
import time
from typing import List
from dataclasses import dataclass

from history_item import HistoryItem

log = logging.getLogger(__name__)

LOCAL_DATA = os.getenv('LOCALAPPDATA')
ROAMING = os.getenv('APPDATA')
CHROME_DIR = Path(LOCAL_DATA, 'Google', 'Chrome', 'User Data', 'Default', 'History')
FIREFOX_DIR = Path(ROAMING, 'Mozilla', 'Firefox', 'Profiles')
FIREFOX_DIR = Path(ROAMING, 'Mozilla', 'Firefox', 'Profiles').glob('*.default-release').__next__()
EDGE_DIR = Path(LOCAL_DATA, 'Microsoft', 'Edge', 'User Data', 'Default', 'History')
BRAVE_DIR = Path(LOCAL_DATA, 'BraveSoftware', 'Brave-Browser', 'User Data', 'Default', 'History')

def get(browser_name):
if browser_name == 'chrome':
return Chrome()
elif browser_name == 'firefox':
return Firefox()
elif browser_name == 'edge':
return Edge()
elif browser_name == 'brave':
return Brave()
else:
raise ValueError('Invalid browser name')

class Base(object):

def __del__(self):
if hasattr(self, 'temp_path'):
# Probably best we don't leave browser history in the temp directory
# This deletes the temporary database file after the object is destroyed
os.remove(self.temp_path)

def _copy_database(self, database_path):
"""
Copies the database to a temporary location and returns the path to the
copy.
"""
temp_dir = gettempdir()
temp_path = shutil.copy(database_path, temp_dir)
self.temp_path = temp_path
return temp_path

def query_history(self, database_path, query, limit=10):
"""
Query Browser history SQL Database.
"""
# Copy the database to a temporary location.
temp_path = self._copy_database(database_path)

# Open the database.
with sqlite3.connect(temp_path) as connection:
cursor = connection.cursor()
cursor.execute(f'{query} LIMIT {limit}')
return cursor.fetchall()

def get_history_items(self, results):
"""
Converts the tuple returned by the query to a HistoryItem object.
"""
data = []
for result in results:
data.append(HistoryItem(self, *result))
return data


class Chrome(Base):
"""Google Chrome History"""

def __init__(self, database_path=CHROME_DIR):
self.database_path = database_path
CHROME_QUERY = 'SELECT url, title, last_visit_time FROM urls ORDER BY last_visit_time DESC'
FIREFOX_QUERY = 'SELECT url, title, visit_date FROM moz_places INNER JOIN moz_historyvisits on moz_historyvisits.place_id = moz_places.id ORDER BY visit_date DESC'

def history(self, limit=10):
"""
Returns a list of the most recently visited sites in Chrome's history.
"""
recents = self.query_history(self.database_path, 'SELECT url, title, last_visit_time FROM urls ORDER BY last_visit_time DESC', limit)
return self.get_history_items(recents)

class Firefox(Base):
"""Firefox Browser History"""
class HistoryDB:
"""
Creates a temporary copy of the browser history database and deletes it when the object is destroyed.

def __init__(self, database_path=FIREFOX_DIR):
# Firefox database is not in a static location, so we need to find it
self.database_path = self.find_database(database_path)
This is necessary because the database is locked when the browser is open.
"""

def find_database(self, path):
"""Find database in path"""
release_folder = Path(path).glob('*.default-release').__next__()
return Path(path, release_folder, 'places.sqlite')
def __init__(self, original_file_path: str):
self.original_file_path = original_file_path

def history(self, limit=10):
"""Most recent Firefox history"""
recents = self.query_history(self.database_path, 'SELECT url, title, visit_date FROM moz_places INNER JOIN moz_historyvisits on moz_historyvisits.place_id = moz_places.id ORDER BY visit_date DESC', limit)
return self.get_history_items(recents)
def __enter__(self):
"""Copy the database to a temporary location and return the path to the copy."""
# Documentation states this is the most secure way to make a temp file.
_temp_file = mktemp() # Documentation: https://docs.python.org/3/library/tempfile.html#tempfile.mktemp
self.temp_file_path = shutil.copyfile(self.original_file_path, _temp_file)
return self.temp_file_path

class Edge(Base):
"""Microsoft Edge History"""

def __init__(self, database_path=EDGE_DIR):
self.database_path = database_path

def history(self, limit=10):
"""
Returns a list of the most recently visited sites in Chrome's history.
"""
recents = self.query_history(self.database_path, 'SELECT url, title, last_visit_time FROM urls ORDER BY last_visit_time DESC', limit)
return self.get_history_items(recents)
def __exit__(self, exc_type, exc_val, exc_tb):
"""Delete the temporary database file after the object is destroyed."""
for _ in range(10):
try:
os.remove(self.temp_file_path)
except PermissionError:
time.sleep(0.5)
else:
break
else:
raise PermissionError(f'Could not delete temp file!')

class Brave(Base):
"""Brave Browser History"""
class Browser:

def __init__(self, database_path=BRAVE_DIR):
def __init__(self, database_path: str = CHROME_DIR, db_query: str = CHROME_QUERY):
self.database_path = database_path
self.db_query = db_query

def history(self, limit=10):
"""
Returns a list of the most recently visited sites in Brave's history.
"""
recents = self.query_history(self.database_path, 'SELECT url, title, last_visit_time FROM urls ORDER BY last_visit_time DESC', limit)
return self.get_history_items(recents)


class HistoryItem(object):
"""Representation of a history item"""

def __init__(self, browser, url, title, last_visit_time):
self.browser = browser
self.url = url
if title is None:
title = ''
if title.strip() == '':
self.title = url
else:
self.title = title
self.last_visit_time = last_visit_time

def timestamp(self):
if isinstance(self.browser, (Chrome)):
return datetime((self.last_visit_time/1000000)-11644473600, 'unixepoch', 'localtime')
elif isinstance(self.browser, (Firefox)):
return datetime.fromtimestamp(self.last_visit_time / 1000000.0)
elif isinstance(self.browser, (Edge)):
return datetime((self.last_visit_time/1000000)-11644473600, 'unixepoch', 'localtime')
elif isinstance(self.browser, (Brave)):
return datetime((self.last_visit_time/1000000)-11644473600, 'unixepoch', 'localtime')
def _query_db(self, db_file: str, limit: int = 10):
"""Query Browser history SQL Database."""
with sqlite3.connect(db_file) as connection:
cursor = connection.cursor()
cursor.execute(f'{self.db_query} LIMIT {limit}')
results = cursor.fetchall()
connection.close() # Context manager doesn't close the db file
return results

def get_history(self, limit: int = 10) -> List[HistoryItem]:
"""Returns a list of the most recently visited sites in Chrome's history."""
with HistoryDB(self.database_path) as db_file:
recents = self._query_db(db_file, limit)
return [HistoryItem(self, *result) for result in recents]

BROWSERS = {
'chrome': Browser(CHROME_DIR, CHROME_QUERY),
'firefox': Browser(FIREFOX_DIR, FIREFOX_QUERY),
'edge': Browser(EDGE_DIR, CHROME_QUERY),
'brave': Browser(BRAVE_DIR, CHROME_QUERY)
}
13 changes: 13 additions & 0 deletions plugin/history_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from plugin.browsers import Browser

@dataclass
class HistoryItem:

browser: 'Browser'
url: str
title: str
last_visit_time: int
41 changes: 26 additions & 15 deletions plugin/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from flox import Flox, ICON_HISTORY, ICON_BROWSER
from flox.string_matcher import string_matcher

import browsers
from browsers import BROWSERS

HISTORY_GLYPH = ''
DEFAULT_BROWSER = 'chrome'
Expand All @@ -15,8 +16,8 @@ class BrowserHistory(Flox):

def __init__(self):
super().__init__()
self.default_browser = self.settings.get('default_browser', DEFAULT_BROWSER)
self.browser = browsers.get(self.default_browser.lower())
self.default_browser = self.settings.get('default_browser', DEFAULT_BROWSER).lower()
self.browser = BROWSERS[self.default_browser]

def _query(self, query):
try:
Expand All @@ -26,23 +27,33 @@ def _query(self, query):
title='History not found!',
subtitle='Check your logs for more information.',
)
except Exception as e:
self.add_item(
title='Something went wrong!',
subtitle=f'Check your logs for more information: {e}',
)
self.logger.exception(e)
finally:
return self._results

def query(self, query):
history = self.browser.history(limit=10000)
history = self.browser.get_history(limit=10000)
for idx, item in enumerate(history):
if query.lower() in item.title.lower() or query.lower() in item.url.lower():
subtitle = f"{idx}. {item.url}"
self.add_item(
title=item.title,
subtitle=subtitle,
icon=ICON_HISTORY,
glyph=HISTORY_GLYPH,
method=self.browser_open,
parameters=[item.url],
context=[item.title, item.url]
)
title_match = string_matcher(query, item.title)
title_score = title_match.score if title_match else 0
url_match = string_matcher(query, item.url)
url_score = url_match.score if url_match else 0
subtitle = f"{idx}. {item.url}"
self.add_item(
title=item.title,
subtitle=subtitle,
icon=ICON_HISTORY,
glyph=HISTORY_GLYPH,
method=self.browser_open,
parameters=[item.url],
context=[item.title, item.url],
score=int(max(title_score, url_score)),
)

def context_menu(self, data):
self.add_item(
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
flox-lib==0.10.4
flox-lib==0.19.1