|
| 1 | +from datetime import datetime |
| 2 | +from os import name, getenv |
| 3 | +from json import loads |
| 4 | +from re import compile, IGNORECASE, sub |
| 5 | +from pathlib import Path |
| 6 | +from configparser import ConfigParser |
| 7 | +from argparse import ArgumentParser |
| 8 | +from shutil import copytree, ignore_patterns |
| 9 | +from urllib.request import urlopen |
| 10 | +from subprocess import check_output |
| 11 | +from io import BytesIO |
| 12 | +from zipfile import ZipFile |
| 13 | + |
| 14 | +""" |
| 15 | +install(-betterfox).py |
| 16 | +
|
| 17 | +Usage: |
| 18 | + python install.py |
| 19 | + |
| 20 | +
|
| 21 | +When called without arguments, it will: |
| 22 | +- Backup your current firefox profile |
| 23 | +- Automatically download user.js from the latest Betterfox release compatible with your Firefox version into the profile |
| 24 | +- Apply user-overrides in the same directory |
| 25 | +
|
| 26 | +However, you can check out install.py/betterfox-install.exe --help to customise most behaviours! |
| 27 | +
|
| 28 | +Limitations: |
| 29 | +- When using a different repositoy as a source, that repository needs to use the same releases workflow |
| 30 | +- Over time, the get_releases might not list older releases due to limited page size. This can be expanded down the road, though |
| 31 | +
|
| 32 | +Building into an exe (on Windows): |
| 33 | +- pipx install pyinstaller (note: you can try without pipx, but this didn't work for me) |
| 34 | +- Run: |
| 35 | + - CMD: `pyinstaller --onefile --name install-betterfox install.py && move %cd%\dist\install-betterfox.exe %cd% && del install-betterfox.spec && rmdir /S /Q build && rmdir dist` |
| 36 | + - BASH: `pyinstaller --onefile --name install-betterfox install.py && && mv dist/install-betterfox.exe . && rm install-betterfox.spec && rm -rf ./build/ && rmdir dist` |
| 37 | + (Sorry, didn't want to add a .gitignore solely for the install script) |
| 38 | +- Done! |
| 39 | +
|
| 40 | +If there's any problems with the script, feel free to mention @Denperidge on GitHub! |
| 41 | +""" |
| 42 | + |
| 43 | +re_find_version = compile(r"mozilla.org/.*?/firefox/(?P<version>[\d.]*?)/", IGNORECASE) |
| 44 | +re_find_overrides = r"(overrides|prefs).*\n(?P<space>\n)" |
| 45 | + |
| 46 | +FIREFOX_ROOT = Path.home().joinpath(".mozilla/firefox").absolute() if name != "nt" else Path(getenv("APPDATA") + "/Mozilla/Firefox/").resolve() |
| 47 | +DEFAULT_FIREFOX_INSTALL = Path("C:/Program Files/Mozilla Firefox/" if name == "nt" else "") |
| 48 | + |
| 49 | +selected_if_backup = None |
| 50 | +selected_config = "" |
| 51 | +userjs_path = None |
| 52 | + |
| 53 | + |
| 54 | +def _get_firefox_version(bin="firefox"): |
| 55 | + try: |
| 56 | + ver_string = check_output([bin, "--version"], encoding="UTF-8") |
| 57 | + return ver_string[ver_string.rindex(" ")+1:].strip() |
| 58 | + except FileNotFoundError: |
| 59 | + return _get_firefox_version(str(DEFAULT_FIREFOX_INSTALL.joinpath("firefox"))) |
| 60 | + |
| 61 | +def _get_default_profile_folder(): |
| 62 | + config_path = FIREFOX_ROOT.joinpath("profiles.ini") |
| 63 | + |
| 64 | + print(f"Reading {config_path}...") |
| 65 | + |
| 66 | + config_parser = ConfigParser(strict=False) |
| 67 | + config_parser.read(config_path) |
| 68 | + |
| 69 | + for section in config_parser.sections(): |
| 70 | + if "Default" in config_parser[section]: |
| 71 | + if config_parser[section]["Default"] == "1": |
| 72 | + print("Default detected: " + section) |
| 73 | + return FIREFOX_ROOT.joinpath(config_parser[section]["Path"]) |
| 74 | + |
| 75 | + |
| 76 | +def _get_releases(repository_owner, repository_name): |
| 77 | + releases = [] |
| 78 | + raw_releases = loads(urlopen(f"https://api.github.com/repos/{repository_owner}/{repository_name}/releases").read()) |
| 79 | + for raw_release in raw_releases: |
| 80 | + name = raw_release["name"] or raw_release["tag_name"] # or fixes 126.0 not being lodaded |
| 81 | + body = raw_release["body"] |
| 82 | + |
| 83 | + |
| 84 | + # Find which firefox releases are supported. Manual overrides for ones that don't have it written in their thing! |
| 85 | + if name == "user.js v.122.1": |
| 86 | + supported = ["107.0", "107.1", "108.0", "108.0.1", "108.0.2", "109.0", "109.0", "110.1", "110.0.1", "111.0", "111.0.1", "112.0", "112.0.1", "112.0.2", "113.0", "113.0.1", "113.0.2", "114.0", "114.0.1", "114.0.2", "115.0", "115.0.1", "115.0.2", "115.0.3", "115.1.0", "115.10.0", "115.11.0", "115.12.0", "115.13.0", "115.14.0", "115.15.0", "115.16.0", "115.16.1", "115.17.0", "115.2.0", "115.2.1", "115.3.0", "115.3.1", "115.4.0", "115.5.0", "115.6.0", "115.7.0", "115.8.0", "115.9.0", "115.9.1", "116.0", "116.0.1", "116.0.2", "116.0.3", "117.0", "117.0.1", "118.0", "118.0.1", "118.0.2", "119.0", "119.0.1", "120.0", "120.0.1", "121.0", "121.0.1", "122.0", "122.0.1"] |
| 87 | + elif name == "user.js 116.1": |
| 88 | + supported = ["116.0", "116.0.1", "116.0.2", "116.0.3"] |
| 89 | + elif name == "Betterfox v.107": |
| 90 | + supported = ["107.0"] |
| 91 | + elif "firefox release" in body.lower(): |
| 92 | + trim_body = body.lower()[body.lower().index("firefox release"):] |
| 93 | + supported = re_find_version.findall(trim_body) |
| 94 | + if len(supported) == 0: |
| 95 | + print(f"Could not parse release in '{name}'. Please post this error message on https://github.com/{repository_owner}/{repository_name}/issues") |
| 96 | + continue |
| 97 | + else: |
| 98 | + print(f"Could not find firefox release header '{name}'. Please post this error message on https://github.com/{repository_owner}/{repository_name}/issues") |
| 99 | + continue |
| 100 | + |
| 101 | + releases.append({ |
| 102 | + "name": name, |
| 103 | + "url": raw_release["zipball_url"], |
| 104 | + "supported": supported, |
| 105 | + }) |
| 106 | + return releases |
| 107 | + |
| 108 | +def _get_latest_compatible_release(releases): |
| 109 | + for release in releases: |
| 110 | + if firefox_version in release["supported"]: |
| 111 | + return release |
| 112 | + return None |
| 113 | + |
| 114 | + |
| 115 | +def backup_profile(src): |
| 116 | + dest = f"{src}-backup-{datetime.today().strftime('%Y-%m-%d-%H-%M-%S')}" |
| 117 | + |
| 118 | + copytree(src, dest, ignore=ignore_patterns("*lock")) |
| 119 | + print("Backed up profile to " + dest) |
| 120 | + |
| 121 | + |
| 122 | +def download_betterfox(url): |
| 123 | + data = BytesIO() |
| 124 | + data.write(urlopen(url).read()) |
| 125 | + return data |
| 126 | + |
| 127 | +def extract_betterfox(data, profile_folder): |
| 128 | + zipfile = ZipFile(data) |
| 129 | + userjs_zipinfo = None |
| 130 | + for file in zipfile.filelist: |
| 131 | + if file.filename.endswith("user.js"): |
| 132 | + userjs_zipinfo = file |
| 133 | + userjs_zipinfo.filename = Path(userjs_zipinfo.filename).name |
| 134 | + |
| 135 | + if not userjs_zipinfo: |
| 136 | + raise BaseException("Could not find user.js!") |
| 137 | + |
| 138 | + return zipfile.extract(userjs_zipinfo, profile_folder) |
| 139 | + |
| 140 | + |
| 141 | +def list_releases(releases, only_supported=False, add_index=False): |
| 142 | + print() |
| 143 | + print(f"Listing {'compatible' if only_supported else 'all'} Betterfox releases:") |
| 144 | + if only_supported: |
| 145 | + print("Use --list-all to view all available releases") |
| 146 | + else: |
| 147 | + print(f"Releases marked with '> ' are documented to be compatible with your Firefox version ({firefox_version})") |
| 148 | + print() |
| 149 | + |
| 150 | + i = 0 |
| 151 | + for release in releases: |
| 152 | + supported = firefox_version in release["supported"] |
| 153 | + if not only_supported or (only_supported and supported): |
| 154 | + print(f"{f'[{i}]' if add_index else ''}{'> ' if supported else ' '}{release['name'].ljust(20)}\t\t\tSupported: {','.join(release['supported'])}") |
| 155 | + i+=1 |
| 156 | + |
| 157 | + |
| 158 | +if __name__ == "__main__": |
| 159 | + firefox_version = _get_firefox_version() |
| 160 | + selected_release = None |
| 161 | + |
| 162 | + default_profile_folder = _get_default_profile_folder() |
| 163 | + argparser = ArgumentParser( |
| 164 | + |
| 165 | + ) |
| 166 | + argparser.add_argument("--overrides", "-o", default=default_profile_folder.joinpath("user-overrides.js"), help="if the provided file exists, add overrides to user.js. Defaults to " + str(default_profile_folder.joinpath("user-overrides.js"))), |
| 167 | + |
| 168 | + |
| 169 | + advanced = argparser.add_argument_group("Advanced") |
| 170 | + advanced.add_argument("--betterfox-version", "-bv", default=None, help=f"Which version of Betterfox to install. Defaults to the latest compatible release for your installed Firefox version") |
| 171 | + advanced.add_argument("--profile-dir", "-p", "-pd", default=default_profile_folder, help=f"Which profile dir to install user.js in. Defaults to {default_profile_folder}") |
| 172 | + advanced.add_argument("--repository-owner", "-ro", default="yokoffing", help="owner of the Betterfox repository. Defaults to yokoffing") |
| 173 | + advanced.add_argument("--repository-name", "-rn", default="Betterfox", help="name of the Betterfox repository. Defaults to Betterfox") |
| 174 | + |
| 175 | + disable = argparser.add_argument_group("Disable functionality") |
| 176 | + disable.add_argument("--no-backup", "-nb", action="store_true", default=False, help="disable backup of current profile (not recommended)"), |
| 177 | + disable.add_argument("--no-install", "-ni", action="store_true", default=False, help="don't install Betterfox"), |
| 178 | + |
| 179 | + modes = argparser.add_mutually_exclusive_group() |
| 180 | + modes.add_argument("--list", action="store_true", default=False, help=f"List all Betterfox releases compatible with your version of Firefox ({firefox_version})") |
| 181 | + modes.add_argument("--list-all", action="store_true", default=False, help=f"List all Betterfox releases") |
| 182 | + modes.add_argument("--interactive", "-i", action="store_true", default=False, help=f"Interactively select Betterfox version") |
| 183 | + |
| 184 | + args = argparser.parse_args() |
| 185 | + |
| 186 | + releases = _get_releases(args.repository_owner, args.repository_name) |
| 187 | + |
| 188 | + |
| 189 | + if args.list or args.list_all: |
| 190 | + list_releases(releases, args.list) |
| 191 | + input("Press ENTER to exit...") |
| 192 | + exit() |
| 193 | + |
| 194 | + if not args.no_backup: |
| 195 | + backup_profile(args.profile_dir) |
| 196 | + |
| 197 | + if args.betterfox_version: |
| 198 | + # If not None AND not string, default value has been used |
| 199 | + if not isinstance(args.betterfox_version, str): |
| 200 | + selected_release = args.betterfox_version |
| 201 | + print(f"Using latest compatible Betterfox version ({selected_release['name']})...") |
| 202 | + # If string has been passed |
| 203 | + else: |
| 204 | + selected_release = next(rel for rel in releases if rel['name'] == args.betterfox_version) |
| 205 | + print(f"Using manually selected Betterfox version ({selected_release['name']})") |
| 206 | + |
| 207 | + if not args.betterfox_version: |
| 208 | + selected_release = _get_latest_compatible_release(releases) |
| 209 | + |
| 210 | + if args.interactive or not selected_release: |
| 211 | + if not selected_release: |
| 212 | + print("Could not find a compatible Betterfox version for your Firefox installation.") |
| 213 | + |
| 214 | + list_releases(releases, False, True) |
| 215 | + selection = int(input(f"Select Betterfox version, or press enter without typing a number to cancel [0-{len(releases) - 1}]: ")) |
| 216 | + |
| 217 | + selected_release = releases[selection] |
| 218 | + |
| 219 | + |
| 220 | + |
| 221 | + if not args.no_install: |
| 222 | + userjs_path = extract_betterfox( |
| 223 | + download_betterfox(selected_release["url"]), |
| 224 | + args.profile_dir |
| 225 | + ) |
| 226 | + print(f"Installed user.js to {userjs_path} !") |
| 227 | + |
| 228 | + |
| 229 | + if Path(args.overrides).exists(): |
| 230 | + print("Found overrides at " + str(args.overrides)) |
| 231 | + |
| 232 | + with open(str(args.overrides), "r", encoding="utf-8") as overrides_file: |
| 233 | + overrides = overrides_file.read() |
| 234 | + with open(userjs_path, "r", encoding="utf-8") as userjs_file: |
| 235 | + old_content = userjs_file.read() |
| 236 | + new_content = sub(re_find_overrides, "\n" + overrides + "\n", old_content, count=1, flags=IGNORECASE) |
| 237 | + with open(userjs_path, "w", encoding="utf-8") as userjs_file: |
| 238 | + userjs_file.write(new_content) |
| 239 | + else: |
| 240 | + print(f"Found no overrides in {args.overrides}") |
| 241 | + |
| 242 | + input("Press ENTER to exit...") |
| 243 | + |
0 commit comments