From b515195c95b22bde26138efbd8bcc2871cebd33d Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Thu, 29 Dec 2022 14:01:55 -0500 Subject: [PATCH 1/4] Use dictionary for class instances --- plugin/browsers.py | 183 +++++++++++++---------------------------- plugin/history_item.py | 13 +++ plugin/main.py | 9 +- 3 files changed, 74 insertions(+), 131 deletions(-) create mode 100644 plugin/history_item.py diff --git a/plugin/browsers.py b/plugin/browsers.py index 36eaf95..1534fff 100644 --- a/plugin/browsers.py +++ b/plugin/browsers.py @@ -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) +} \ No newline at end of file diff --git a/plugin/history_item.py b/plugin/history_item.py new file mode 100644 index 0000000..63ef210 --- /dev/null +++ b/plugin/history_item.py @@ -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 \ No newline at end of file diff --git a/plugin/main.py b/plugin/main.py index 5450565..12560b5 100644 --- a/plugin/main.py +++ b/plugin/main.py @@ -1,6 +1,6 @@ from flox import Flox, ICON_HISTORY, ICON_BROWSER -import browsers +from browsers import BROWSERS HISTORY_GLYPH = '' DEFAULT_BROWSER = 'chrome' @@ -15,9 +15,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: self.query(query) @@ -30,7 +29,7 @@ def _query(self, query): 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}" From 378b1d1a93cd41908aeebcb08d6fc72a0c6bc178 Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Thu, 29 Dec 2022 14:02:12 -0500 Subject: [PATCH 2/4] Major version update --- plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.json b/plugin.json index 73daacb..bb67b41 100644 --- a/plugin.json +++ b/plugin.json @@ -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", From ace8b8ebb1cf3b1475565efd33c86ad31b75cffa Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Thu, 29 Dec 2022 14:03:17 -0500 Subject: [PATCH 3/4] Update flox-lib to 0.19.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 151f5b3..f4a0166 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -flox-lib==0.10.4 +flox-lib==0.19.1 From 547a61d01b9e88e5fe10ca3420597f35a1476a1f Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Thu, 29 Dec 2022 14:12:02 -0500 Subject: [PATCH 4/4] Use Flox/Flows string matching score --- plugin/main.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/plugin/main.py b/plugin/main.py index 12560b5..b83ea17 100644 --- a/plugin/main.py +++ b/plugin/main.py @@ -1,4 +1,5 @@ from flox import Flox, ICON_HISTORY, ICON_BROWSER +from flox.string_matcher import string_matcher from browsers import BROWSERS @@ -17,6 +18,7 @@ def __init__(self): super().__init__() self.default_browser = self.settings.get('default_browser', DEFAULT_BROWSER).lower() self.browser = BROWSERS[self.default_browser] + def _query(self, query): try: self.query(query) @@ -25,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.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(