mirror of
https://gitlab.com/ceda_ei/firefox-web-apps
synced 2025-12-03 17:30:07 +01:00
Compare commits
9 Commits
2b1d542b2a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| dce0da91b2 | |||
| 9137953537 | |||
| 79ef6da558 | |||
| ffcdeedbc9 | |||
| d763d300fd | |||
| 946317aa76 | |||
| d1da791858 | |||
| 9fdb906183 | |||
| 7bf3b19100 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,4 @@
|
|||||||
.firefox_profile
|
.firefox_profile
|
||||||
|
__pycache__/
|
||||||
|
icons/
|
||||||
|
bin/
|
||||||
|
|||||||
206
create_app.py
Executable file
206
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()
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
beautifulsoup4>=4.0
|
||||||
|
requests>=2.0
|
||||||
10
setup.sh
10
setup.sh
@@ -5,7 +5,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
REPO_DIR="$(dirname "$0")"
|
REPO_DIR="$(dirname "$0")"
|
||||||
BIN_DIR="$REPO_DIR/bin"
|
BIN_DIR="$REPO_DIR/bin"
|
||||||
ICON_DIR="$REPO_DIR/icon"
|
ICON_DIR="$REPO_DIR/icons"
|
||||||
FIRST_LAUNCH="https://gitlab.com/ceda_ei/firefox-web-apps/-/wikis/Getting-Started"
|
FIRST_LAUNCH="https://gitlab.com/ceda_ei/firefox-web-apps/-/wikis/Getting-Started"
|
||||||
HELP_TEXT="
|
HELP_TEXT="
|
||||||
Usage:
|
Usage:
|
||||||
@@ -75,7 +75,7 @@ if [[ $FIREFOX_PROFILE == "" ]] || (( NEW == 1 )); then
|
|||||||
echo -n "Use an existing profile for apps? (y/N): "
|
echo -n "Use an existing profile for apps? (y/N): "
|
||||||
read -r input
|
read -r input
|
||||||
if [[ ${input^^} == "Y" ]]; then
|
if [[ ${input^^} == "Y" ]]; then
|
||||||
echo "Enter path to existing profile (or run the script with --firefox_profile): "
|
echo -n "Enter path to existing profile (or run the script with --firefox_profile): "
|
||||||
read -r FIREFOX_PROFILE
|
read -r FIREFOX_PROFILE
|
||||||
else
|
else
|
||||||
NEW=1
|
NEW=1
|
||||||
@@ -113,13 +113,13 @@ read -r
|
|||||||
|
|
||||||
mkdir "$FIREFOX_PROFILE/chrome" &> /dev/null || true
|
mkdir "$FIREFOX_PROFILE/chrome" &> /dev/null || true
|
||||||
HIDDEN_SELECTORS=()
|
HIDDEN_SELECTORS=()
|
||||||
echo -e "Do you want to hide tabs? (y/N)"
|
echo -n "Do you want to hide tabs? (y/N) "
|
||||||
read -r input
|
read -r input
|
||||||
if [[ ${input^^} == "Y" ]]; then
|
if [[ ${input^^} == "Y" ]]; then
|
||||||
HIDDEN_SELECTORS=("${HIDDEN_SELECTORS[@]}" "#tabbrowser-tabs")
|
HIDDEN_SELECTORS=("${HIDDEN_SELECTORS[@]}" "#tabbrowser-tabs")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "Do you want to hide main toolbar (address bar, back, forward, etc)? (y/N)"
|
echo -n "Do you want to hide main toolbar (address bar, back, forward, etc)? (y/N) "
|
||||||
read -r input
|
read -r input
|
||||||
if [[ ${input^^} == "Y" ]]; then
|
if [[ ${input^^} == "Y" ]]; then
|
||||||
HIDDEN_SELECTORS=("${HIDDEN_SELECTORS[@]}" "#nav-bar")
|
HIDDEN_SELECTORS=("${HIDDEN_SELECTORS[@]}" "#nav-bar")
|
||||||
@@ -137,3 +137,5 @@ if (( ${#HIDDEN_SELECTORS[@]} > 0 )); then
|
|||||||
visibility: collapse !important;
|
visibility: collapse !important;
|
||||||
}" >> "$FIREFOX_PROFILE/chrome/userChrome.css"
|
}" >> "$FIREFOX_PROFILE/chrome/userChrome.css"
|
||||||
fi
|
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