mirror of
https://gitlab.com/ceda_ei/firefox-web-apps
synced 2025-11-04 19:20:06 +01:00
Add poetry and move everything into a module (fwa)
This commit is contained in:
0
fwa/__init__.py
Normal file
0
fwa/__init__.py
Normal file
206
fwa/create_app.py
Executable file
206
fwa/create_app.py
Executable file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"Creates a firefox web app"
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import shlex
|
||||
import stat
|
||||
import sys
|
||||
import os
|
||||
import os.path as pt
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
from collections import Counter
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from PIL import Image
|
||||
|
||||
|
||||
REPO_DIR = pt.abspath(pt.dirname(sys.argv[0]))
|
||||
BIN_DIR = f"{REPO_DIR}/bin"
|
||||
ICON_DIR = f"{REPO_DIR}/icons"
|
||||
|
||||
|
||||
def eprint(*args, **kwargs):
|
||||
"Print an error"
|
||||
print(*args, **kwargs, file=sys.stderr)
|
||||
|
||||
|
||||
def url_exists(url):
|
||||
"Tests if the url exists after all redirects"
|
||||
return requests.head(url, allow_redirects=True).status_code == 200
|
||||
|
||||
|
||||
def absolute_url(base_url, relative_url):
|
||||
"Returns the absolute_url if the relative_url is relative"
|
||||
base = urlparse(base_url)
|
||||
relative = urlparse(relative_url)
|
||||
|
||||
# Make the url absolute if it is not
|
||||
if not relative.hostname:
|
||||
return urlunparse((*base[:2], *relative[2:]))
|
||||
return relative_url
|
||||
|
||||
|
||||
# pylint: disable=W0613
|
||||
def create_webapp(name, url, exec_name, logo, profile):
|
||||
"Creates the necessary files for the webapp."
|
||||
local_vars = locals()
|
||||
# Create a dictionary with shell-quoted version of arguments
|
||||
quoted = {i: shlex.quote(local_vars[i]) for i in ("name", "url", "exec_name",
|
||||
"logo", "profile")}
|
||||
# Download and convert the logo
|
||||
logo_pt = f"{ICON_DIR}/{exec_name}.png"
|
||||
res = requests.get(logo, allow_redirects=True)
|
||||
with Image.open(io.BytesIO(res.content)) as img:
|
||||
img.save(logo_pt, "PNG")
|
||||
|
||||
# Create the binary
|
||||
script_pt = f"{BIN_DIR}/{exec_name}"
|
||||
with open(script_pt, "w") as script:
|
||||
script.write("#!/usr/bin/env sh\n")
|
||||
script.write(f"firefox --profile {quoted['profile']} {quoted['url']}\n")
|
||||
os.chmod(script_pt, os.stat(script_pt).st_mode | stat.S_IXUSR | stat.S_IXGRP)
|
||||
|
||||
# Create the desktop file
|
||||
desk_pt = pt.expanduser(f"~/.local/share/applications/{exec_name}.desktop")
|
||||
with open(desk_pt, "w") as desktop:
|
||||
desktop.write(f"""
|
||||
[Desktop Entry]
|
||||
Name={name} (Web App)
|
||||
Exec={script_pt}
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon={logo_pt}
|
||||
Categories=Network;X-WebApps
|
||||
""".strip() + "\n")
|
||||
|
||||
|
||||
def extract_metadata(url):
|
||||
"Extract metadata using bs4"
|
||||
# Get and parse the page
|
||||
content = requests.get(url, allow_redirects=True).content
|
||||
soup = BeautifulSoup(content, 'html.parser')
|
||||
metadata = {}
|
||||
|
||||
# Find the title
|
||||
titles = []
|
||||
if soup.title:
|
||||
titles = [soup.title.string]
|
||||
for tag in soup.find_all("meta"):
|
||||
title_props = ["title", "og:title", "twitter:title"]
|
||||
if tag.get("property", None) in title_props \
|
||||
or tag.get("name", None) in title_props:
|
||||
titles.append(tag["content"])
|
||||
# Set title to the most common if it occurs more than once, else prefer
|
||||
# title tag
|
||||
most_common = Counter(titles).most_common(1)
|
||||
if not most_common:
|
||||
metadata["title"] = None
|
||||
elif most_common[0][1] > 1:
|
||||
metadata["title"] = most_common[0][0].strip()
|
||||
else:
|
||||
if soup.title:
|
||||
metadata["title"] = soup.title.string.strip()
|
||||
else:
|
||||
metadata["title"] = most_common[0][0].strip()
|
||||
|
||||
# Find the image.
|
||||
# Try link first, followed by /favicon.{png,ico}, followed by og:, twitter:
|
||||
image = None
|
||||
for favicon in soup.find_all("link", rel="icon"):
|
||||
if url_exists(absolute_url(url, favicon["href"])):
|
||||
image = absolute_url(url, favicon["href"])
|
||||
|
||||
if not image:
|
||||
for favicon in [absolute_url(url, i) for i in ("favicon.png", "favicon.ico")]:
|
||||
if requests.head(favicon, allow_redirects=True).status_code == 200:
|
||||
image = favicon
|
||||
break
|
||||
|
||||
if not image:
|
||||
for prop in ["og:image", "twitter:image"]:
|
||||
prop_tag = soup.find("meta", property=prop)
|
||||
if prop_tag and url_exists(absolute_url(url, prop_tag["content"])):
|
||||
image = absolute_url(url, prop_tag["content"])
|
||||
break
|
||||
|
||||
metadata["image"] = image
|
||||
return metadata
|
||||
|
||||
|
||||
def main():
|
||||
"Main Function"
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("url", help="URL for the webapp")
|
||||
parser.add_argument(
|
||||
"-n", "--name",
|
||||
help=("Name of the app as shown in the menu. In absence of this, the "
|
||||
"title of page will be used.")
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-e", "--exec-name",
|
||||
help="Name of the script that will be created in binary directory."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-l", "--logo",
|
||||
help="URL/path for the logo. If omitted, the favicon will be used."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-f", "--firefox-profile",
|
||||
help="Firefox Profile path. If omitted, the default profile is used"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Add Missing Arguments with default values
|
||||
if args.firefox_profile is None:
|
||||
profile_path = REPO_DIR + "/.firefox_profile"
|
||||
with open(profile_path) as prof:
|
||||
args.firefox_profile = prof.readline()[:-1]
|
||||
|
||||
parsed_url = urlparse(args.url)
|
||||
if not parsed_url.scheme:
|
||||
eprint("Missing URL scheme")
|
||||
eprint(f"Maybe you meant https://{args.url} ?")
|
||||
sys.exit(1)
|
||||
|
||||
print("Fetching details ...")
|
||||
metadata = extract_metadata(args.url)
|
||||
if not args.name:
|
||||
args.name = metadata["title"]
|
||||
if not args.logo:
|
||||
args.logo = metadata["image"]
|
||||
if not args.exec_name:
|
||||
args.exec_name = parsed_url.hostname.replace(".", "-") + "-webapp"
|
||||
|
||||
if "/" in args.exec_name:
|
||||
eprint("Executable name can't contain slashes.")
|
||||
sys.exit(2)
|
||||
|
||||
if pt.exists(f"{BIN_DIR}/{args.exec_name}"):
|
||||
index = 0
|
||||
while True:
|
||||
if not pt.exists(f"{BIN_DIR}/{args.exec_name}-{index}"):
|
||||
args.exec_name = f"{args.exec_name}-{index}"
|
||||
break
|
||||
index += 1
|
||||
print()
|
||||
print(f"WebApp Name:\t\t{args.name}")
|
||||
print(f"WebApp URL:\t\t{args.url}")
|
||||
print(f"Logo URL:\t\t{args.logo}")
|
||||
print(f"Executable Name:\t{args.exec_name}")
|
||||
print(f"Firefox Profile:\t{args.firefox_profile}")
|
||||
print()
|
||||
print("Do you want to create the app with the above details (Y/n): ",
|
||||
end=' ')
|
||||
inp = input()
|
||||
if not inp or inp[0].upper() != "N":
|
||||
create_webapp(args.name, args.url, args.exec_name, args.logo,
|
||||
args.firefox_profile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
141
fwa/setup.sh
Executable file
141
fwa/setup.sh
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bash
|
||||
# Usage: ./setup.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(dirname "$0")"
|
||||
BIN_DIR="$REPO_DIR/bin"
|
||||
ICON_DIR="$REPO_DIR/icons"
|
||||
FIRST_LAUNCH="https://gitlab.com/ceda_ei/firefox-web-apps/-/wikis/Getting-Started"
|
||||
HELP_TEXT="
|
||||
Usage:
|
||||
$0 [-f|--firefox-profile] <firefox_profile> [-n|--new] <profile_name> [-h|--help]
|
||||
|
||||
Configure a firefox profile for web apps.
|
||||
|
||||
Options:
|
||||
-f, --firefox-profile Path to an existing firefox profile (unless -n is
|
||||
<firefox_profile> also provided)
|
||||
-n, --new <profile_name> Creates a new profile with the given name. -f
|
||||
configures the new profile path when passed along
|
||||
with -n
|
||||
-h, --help This help page
|
||||
"
|
||||
|
||||
[[ -d $BIN_DIR ]] || mkdir -- "$BIN_DIR"
|
||||
[[ -d $ICON_DIR ]] || mkdir -- "$ICON_DIR"
|
||||
|
||||
FIREFOX_PROFILE=""
|
||||
PROFILE_NAME="firefox-web-apps"
|
||||
NEW=0
|
||||
OPTIONS=f:n:h
|
||||
LONGOPTS=firefox-profile:,new:,help
|
||||
PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
|
||||
eval set -- "$PARSED"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-f|--firefox-profile)
|
||||
shift
|
||||
FIREFOX_PROFILE="$1"
|
||||
shift
|
||||
;;
|
||||
-n|--new)
|
||||
NEW=1
|
||||
shift
|
||||
PROFILE_NAME="$1"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "$HELP_TEXT"
|
||||
exit
|
||||
;;
|
||||
--)
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Error parsing arguments!"
|
||||
exit 1
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if firefox is running
|
||||
if pidof firefox &> /dev/null; then
|
||||
echo "It is recommended to close firefox before running this script."
|
||||
echo -n "Do you want to run the script anyways? (y/N): "
|
||||
read -r input
|
||||
if [[ ${input^^} != "Y" ]]; then
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Prompt to create Firefox profile
|
||||
if [[ $FIREFOX_PROFILE == "" ]] || (( NEW == 1 )); then
|
||||
if (( NEW == 0 )); then
|
||||
echo -n "Use an existing profile for apps? (y/N): "
|
||||
read -r input
|
||||
if [[ ${input^^} == "Y" ]]; then
|
||||
echo -n "Enter path to existing profile (or run the script with --firefox_profile): "
|
||||
read -r FIREFOX_PROFILE
|
||||
else
|
||||
NEW=1
|
||||
fi
|
||||
fi
|
||||
if (( NEW == 1 )); then
|
||||
FIREFOX_PROFILE="${FIREFOX_PROFILE:-$HOME/.mozilla/firefox/${PROFILE_NAME}}"
|
||||
firefox -CreateProfile "${PROFILE_NAME} ${FIREFOX_PROFILE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if firefox_profile is valid
|
||||
if ! [[ -d $FIREFOX_PROFILE ]]; then
|
||||
echo "Invalid Firefox Profile Path"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# Store Profile to be used
|
||||
echo "$FIREFOX_PROFILE" > "$REPO_DIR/.firefox_profile"
|
||||
|
||||
echo "Enabling userChrome.css support"
|
||||
echo -e '\nuser_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);' >> "$FIREFOX_PROFILE/user.js"
|
||||
|
||||
# Starting firefox for customizability
|
||||
firefox --profile "$FIREFOX_PROFILE" "$FIRST_LAUNCH" &
|
||||
|
||||
echo ""
|
||||
echo "Hit Enter once you have completed customizing the profile."
|
||||
read -r
|
||||
|
||||
# userChrome Hacks
|
||||
#
|
||||
# Initially stores all selectors to be hidden in HIDDEN_SELECTORS, followed by
|
||||
# writing a CSS rule that hides them all
|
||||
|
||||
mkdir "$FIREFOX_PROFILE/chrome" &> /dev/null || true
|
||||
HIDDEN_SELECTORS=()
|
||||
echo -n "Do you want to hide tabs? (y/N) "
|
||||
read -r input
|
||||
if [[ ${input^^} == "Y" ]]; then
|
||||
HIDDEN_SELECTORS=("${HIDDEN_SELECTORS[@]}" "#tabbrowser-tabs")
|
||||
fi
|
||||
|
||||
echo -n "Do you want to hide main toolbar (address bar, back, forward, etc)? (y/N) "
|
||||
read -r input
|
||||
if [[ ${input^^} == "Y" ]]; then
|
||||
HIDDEN_SELECTORS=("${HIDDEN_SELECTORS[@]}" "#nav-bar")
|
||||
fi
|
||||
|
||||
function join_by {
|
||||
local IFS="$1";
|
||||
shift;
|
||||
echo -n "$*";
|
||||
}
|
||||
|
||||
if (( ${#HIDDEN_SELECTORS[@]} > 0 )); then
|
||||
join_by , "${HIDDEN_SELECTORS[@]}" >> "$FIREFOX_PROFILE/chrome/userChrome.css"
|
||||
echo "{
|
||||
visibility: collapse !important;
|
||||
}" >> "$FIREFOX_PROFILE/chrome/userChrome.css"
|
||||
fi
|
||||
|
||||
echo "Optional: Add $(cd "$ICON_DIR"; pwd) to your PATH to allowing launching the app from command line"
|
||||
Reference in New Issue
Block a user