Skip to content

Commit

Permalink
Merge pull request #123 from ObaraEmmanuel/feature-default
Browse files Browse the repository at this point in the history
Add default browser support and some refactored functions from __init__ to utils
  • Loading branch information
Samyak2 committed Jan 26, 2021
2 parents 7927531 + de46ff6 commit b28ccf3
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 36 deletions.
1 change: 1 addition & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
good-names=
i,
e,
ignored-modules=winreg
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package to retrieve (almost) any browser's history on (almost) any platform.
- **Bookmarks**: browser bookmarks with timestamp, URL, title and folder.
- Lightweight: the entire package is less than 20kB in size and has no dependencies other than python 3.6+.
- Developer friendly: you can add support for new browsers or add a new feature very easily.
- Default browser: can automatically determine the default browser on Windows and Linux (`browser-history -b default`).
- Fully open source: this project is developed and maintained by [PES Open Source](https://github.com/pesos) and will always be open source (with the Apache 2.0 License).

# Quick Start
Expand Down
30 changes: 2 additions & 28 deletions browser_history/__init__.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,9 @@
import inspect
from . import browsers, generic, utils # noqa: F401


__version__ = "0.3.0"


def get_browsers():
"""This method provides a list of all browsers implemented by
browser_history.
:return: A :py:class:`list` containing implemented browser classes
all inheriting from the super class
:py:class:`browser_history.generic.Browser`
:rtype: :py:class:`list`
"""

# recursively get all concrete subclasses
def get_subclasses(browser):
# include browser itself in return list if it is concrete
sub_classes = []
if not inspect.isabstract(browser):
sub_classes.append(browser)

for sub_class in browser.__subclasses__():
sub_classes.extend(get_subclasses(sub_class))
return sub_classes

return get_subclasses(generic.Browser)


def get_history():
"""This method is used to obtain browser histories of all available and
supported browsers for the system platform.
Expand All @@ -41,7 +15,7 @@ def get_history():
:rtype: :py:class:`browser_history.generic.Outputs`
"""
output_object = generic.Outputs(fetch_type="history")
browser_classes = get_browsers()
browser_classes = utils.get_browsers()
for browser_class in browser_classes:
try:
browser_object = browser_class()
Expand All @@ -64,7 +38,7 @@ def get_bookmarks():
:rtype: :py:class:`browser_history.generic.Outputs`
"""
output_object = generic.Outputs(fetch_type="bookmarks")
subclasses = get_browsers()
subclasses = utils.get_browsers()
for browser_class in subclasses:
try:
browser_object = browser_class()
Expand Down
8 changes: 8 additions & 0 deletions browser_history/browsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Chromium(ChromiumBasedBrowser):
"""

name = "Chromium"
aliases = ("chromiumhtm", "chromium-browser", "chromiumhtml")

linux_path = ".config/chromium"
windows_path = "AppData/Local/chromium/User Data"
Expand All @@ -40,6 +41,7 @@ class Chrome(ChromiumBasedBrowser):
"""

name = "Chrome"
aliases = ("chromehtml", "google-chrome", "chromehtm")

linux_path = ".config/google-chrome"
windows_path = "AppData/Local/Google/Chrome/User Data"
Expand All @@ -61,6 +63,7 @@ class Firefox(Browser):
"""

name = "Firefox"
aliases = ("firefoxurl", )

linux_path = ".mozilla/firefox"
windows_path = "AppData/Roaming/Mozilla/Firefox/Profiles"
Expand Down Expand Up @@ -177,6 +180,7 @@ class Edge(ChromiumBasedBrowser):
"""

name = "Edge"
aliases = ("msedgehtm", "msedge", "microsoft-edge")

windows_path = "AppData/Local/Microsoft/Edge/User Data"
mac_path = "Library/Application Support/Microsoft Edge"
Expand All @@ -195,6 +199,7 @@ class Opera(ChromiumBasedBrowser):
"""

name = "Opera"
aliases = ("operastable", "opera-stable")

linux_path = ".config/opera"
windows_path = "AppData/Roaming/Opera Software/Opera Stable"
Expand All @@ -214,6 +219,7 @@ class OperaGX(ChromiumBasedBrowser):
"""

name = "OperaGX"
aliases = ("operagxstable", "operagx-stable")

windows_path = "AppData/Roaming/Opera Software/Opera GX Stable"

Expand All @@ -233,6 +239,7 @@ class Brave(ChromiumBasedBrowser):
"""

name = "Brave"
aliases = ("bravehtml", )

linux_path = ".config/BraveSoftware/Brave-Browser"
mac_path = "Library/Application Support/BraveSoftware/Brave-Browser"
Expand All @@ -253,6 +260,7 @@ class Vivaldi(ChromiumBasedBrowser):
"""

name = "Vivaldi"
aliases = ("vivaldi-stable", "vivaldistable")

linux_path = ".config/vivaldi"
mac_path = "Library/Application Support/Vivaldi"
Expand Down
20 changes: 13 additions & 7 deletions browser_history/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
browsers,
generic,
get_bookmarks,
get_browsers,
get_history,
utils,
__version__,
)

# get list of all implemented browser by finding subclasses of generic.Browser
AVAILABLE_BROWSERS = ", ".join(b.__name__ for b in get_browsers())
AVAILABLE_BROWSERS = ", ".join(b.__name__ for b in utils.get_browsers())
AVAILABLE_FORMATS = ", ".join(generic.Outputs(fetch_type=None).format_map.keys())
AVAILABLE_TYPES = ", ".join(generic.Outputs(fetch_type=None).field_map.keys())

Expand Down Expand Up @@ -52,7 +51,7 @@ def make_parser():
default="all",
help=f"""
browser to retrieve history or bookmarks from. Should be one
of all, {AVAILABLE_BROWSERS}.
of all, default, {AVAILABLE_BROWSERS}.
Default is all (gets history or bookmarks from all browsers).
""",
)
Expand Down Expand Up @@ -110,10 +109,17 @@ def cli(args):
try:
# gets browser class by name (string).
selected_browser = args.browser
for browser in get_browsers():
if browser.__name__.lower() == args.browser.lower():
selected_browser = browser.__name__
break
if selected_browser == "default":
default = utils.default_browser()
if default is None:
sys.exit(1)
else:
selected_browser = default.__name__
else:
for browser in utils.get_browsers():
if browser.__name__.lower() == args.browser.lower():
selected_browser = browser.__name__
break
browser_class = getattr(browsers, selected_browser)
except AttributeError:
utils.logger.error(
Expand Down
8 changes: 8 additions & 0 deletions browser_history/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Browser(abc.ABC):
* :py:class:`profile_support`
* :py:class:`profile_dir_prefixes`
* :py:class:`_local_tz`
* :py:class:`aliases`: A tuple containing other names for the browser in lowercase
:param plat: the current platform. A value of :py:class:`None` means the platform
will be inferred from the system.
Expand All @@ -57,6 +58,7 @@ class Browser(abc.ABC):
>>> class CustomBrowser(Browser):
... name = 'custom browser'
... aliases = ('custom-browser', 'customhtm')
... history_file = 'history_file'
... history_SQL = \"\"\"
... SELECT
Expand Down Expand Up @@ -93,6 +95,12 @@ class Browser(abc.ABC):
history_dir: Path
"""History directory."""

aliases: tuple = ()
"""Gets possible names (lower-cased) used to refer to the browser type.
Useful for making the browser detectable as a default browser which may be
named in various forms on different platforms. Do not include :py:class:`name`
in this list"""

@property
@abc.abstractmethod
def name(self) -> str:
Expand Down
108 changes: 108 additions & 0 deletions browser_history/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
"""
import enum
import inspect
import logging
import platform
import subprocess

from . import generic

logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
Expand Down Expand Up @@ -48,3 +52,107 @@ def get_platform():
if system == "Windows":
return Platform.WINDOWS
raise NotImplementedError(f"Platform {system} is not supported yet")


def get_browsers():
"""This method provides a list of all browsers implemented by
browser_history.
:return: A :py:class:`list` containing implemented browser classes
all inheriting from the super class
:py:class:`browser_history.generic.Browser`
:rtype: :py:class:`list`
"""

# recursively get all concrete subclasses
def get_subclasses(browser):
# include browser itself in return list if it is concrete
sub_classes = []
if not inspect.isabstract(browser):
sub_classes.append(browser)

for sub_class in browser.__subclasses__():
sub_classes.extend(get_subclasses(sub_class))
return sub_classes

return get_subclasses(generic.Browser)


if get_platform() == Platform.WINDOWS:
import winreg # type: ignore


def _default_browser_linux():
try:
cmd = "xdg-settings get default-web-browser".split()
raw_result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
# most have a suffix ".desktop" so just remove it
default = raw_result.decode().strip().lower().replace(".desktop", "")
except (FileNotFoundError, subprocess.CalledProcessError, PermissionError):
logger.warning("Could not determine default browser")
default = None

return default


def _default_browser_win():
reg_path = (
"Software\\Microsoft\\Windows\\Shell\\Associations\\"
"UrlAssociations\\https\\UserChoice"
)
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path) as key:
default = winreg.QueryValueEx(key, "ProgId")
if default is None:
logger.warning("Could not determine default browser")
return None
return default[0].lower()


def default_browser():
"""This method gets the default browser of the current platform
:return: A :py:class:`browser_history.generic.Browser` object representing the
default browser in the current platform. If platform is not supported or
default browser is unknown or unsupported ``None`` is returned
:rtype: union[:py:class:`browser_history.generic.Browser`, None]
"""
plat = get_platform()

# ---- get default from specific platform ----

# Always try to return a lower-cased value for ease of comparison
if plat == Platform.LINUX:
default = _default_browser_linux()
elif plat == Platform.WINDOWS:
default = _default_browser_win()
else:
logger.warning("Default browser feature not supported on this OS")
return None

if default is None:
# the current platform has completely failed to provide a default
logger.warning("No default browser found")
return None

# ---- convert obtained default to something we understand ----
all_browsers = get_browsers()

# first quick pass for direct matches
for browser in all_browsers:
if default == browser.name.lower() or default in browser.aliases:
return browser

# separate pass for deeper matches
for browser in all_browsers:
# look for alias matches even if the default name has "noise"
# for instance firefox on windows returns something like
# "firefoxurl-3EEDF34567DDE" but we only need "firefoxurl"
for alias in browser.aliases:
if alias in default:
return browser

# nothing was found
logger.warning("Current default browser is not supported")
return None
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# content of pytest.ini
[pytest]
markers =
browser_name: browser name used in default browser tests
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest

from browser_history import get_browsers
from browser_history.utils import get_browsers
from browser_history.cli import cli, AVAILABLE_BROWSERS
from .utils import ( # noqa: F401
become_linux,
Expand Down

0 comments on commit b28ccf3

Please sign in to comment.