Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

F-droid repo web frontend

In my previous post, I showed how I set up my fdroid mirror suitable for adding as an f-droid repository. I wrote a little web page generator, that parses the metadata of the packages.

files/2024/listings/fdroid_generate_web.py (Source)

#!/usr/bin/env python3
# vim: set ts=3 sw=3 sts=3 et:
# File: /etc/installed/fdroid/fdroid_generate_web.py
# Location: server3
# Author: bgstack15
# SPDX-License-Identifier: GPL-3.0-only
# Startdate: 2024-06-03-2 13:31
# Title: Generate simple web view of F-droid repo
# Purpose: Generate a small front-end web page of packages here
# History:
# Usage:
#    called by fdroid-sync.sh, or also just usable by itself
# Reference:
#    https://stackoverflow.com/questions/15940280/how-to-get-utc-time-in-python
# Improve:
# Documentation: design idea is to parse index-v1.json and build a front page, and also a per-app page that resembles f-droid.org/en/packages/org.jellyfin.mobile/
from configobj import ConfigObj
import os, json, datetime
CONF_FILE = os.environ.get("CONF_FILE",os.path.join("/etc","sysconfig","fdroid-generate-web"))
if os.path.exists(CONF_FILE):
   cfg = ConfigObj(CONF_FILE)
else:
   cfg = ConfigObj()
   print(f"Info: could not find CONF_FILE {CONF_FILE}. Using all defaults!")
# defaults
if "TOP_DIR" not in cfg:
   cfg["TOP_DIR"] = "/mnt/mirror/fdroid"
if "MIRROR_DIR" not in cfg:
   cfg["MIRROR_DIR"] = "/mnt/mirror/fdroid/repo"
if "INDEX_JSON" not in cfg:
   cfg["INDEX_JSON"] = "/mnt/mirror/fdroid/repo/index-v1.json"
print(cfg)
def get_app_attribute(app, attribute, localizations = ["en-US","en-GB"]):
   """
   Given app dictionary, get attribute, using the order of localizations if necessary.
   Returns that output, and locale of used value if any.
   """
   output = ""
   try:
      output = app[attribute]
      return output, ""
   except:
      if "localized" in app:
         some_l = app["localized"]
         for lchoice in localizations:
            if lchoice in app["localized"]:
               try:
                  output = app["localized"][lchoice][attribute]
                  return output, lchoice
               except:
                  pass
               some_l.pop(lchoice)
         for l in some_l:
            try:
               output = app["localized"][l][attribute]
               return output, l
            except:
               pass
      else:
         pass
   return output, ""
def get_app_icon(app, packageName, mirror_dir, localizations = ["en-US","en-GB"]):
   icon, localized = get_app_attribute(app, "icon", localizations)
   if localized:
      icon = f"{packageName}/{localized}/{icon}"
   else:
      icon = f"icons/{icon}"
   if not os.path.exists(os.path.join(mirror_dir,icon)):
      if os.path.exists(os.path.join(mirror_dir,packageName,"icon.png")):
         return f"{packageName}/icon.png"
      else:
         for l in localizations:
            if os.path.exists(os.path.join(mirror_dir,packageName,l,"icon.png")):
               return os.path.join(packageName,l,"icon.png")
   return icon
def generate_index_html(json_file, top_dir, mirror_dir):
   """
   Given the index-v1.json file, generate a top-level page for the front page, stored in top_dir.
   """
   with open(json_file,"r") as o:
      j = json.load(o)
   #print(len(json_contents))
   with open(os.path.join(top_dir,"index.html"),"w") as w:
      w.write("<html>\n")
      w.write("<head>\n")
      w.write("<link rel='stylesheet' href='fdroid.css'>\n")
      w.write("""<meta name="viewport" content="width=device-width, initial-scale=1">\n""")
      w.write(f"<title>{j['repo']['name']}</title>\n")
      w.write("</head>\n")
      w.write("<body>\n")
      w.write(f"<h1><img src='repo/icons/{j['repo']['icon']}' class='headimg'>{j['repo']['name']}</h1>\n")
      w.write(f"<p><a href='repo/'>Instructions to install this repository</a></p>")
      #apps = sorted(j["apps"],key=lambda i: i["name"])
      apps = j["apps"]
      if len(apps):
         w.write(f"<h2>Packages ({len(apps)})</h2>")
      for app in apps:
         name, _ = get_app_attribute(app,"name")
         packageName, _ = get_app_attribute(app,"packageName")
         icon = get_app_icon(app, packageName, mirror_dir)
         desc, _ = get_app_attribute(app,"summary")
         #w.write(f"<a href='packages/{packageName}.html'><img src='repo/{icon}'>{name}</a> {desc}<br/>\n")
         w.write(f"<img src='repo/{icon}' class='img'><b>{name}</b> {desc}<br/>\n")
      w.write("</body>\n")
      w.write("<footer>\n")
      w.write(f"Last generated: " + str(datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")))
      w.write("</footer>\n")
      w.write("</html>")
def generate_app_pages(json_file, top_dir):
   with open(json_file,"r") as o:
      j = json.load(o)
   apps = j["apps"]
   for app in apps:
      name, _ = get_app_attribute(app,"name")
      packageName, _ = get_app_attribute(app,"packageName")
      icon, localized = get_app_attribute(app,"icon")
      if localized:
         icon = f"../repo/{packageName}/{localized}/{icon}"
      else:
         icon = f"../repo/icons/{icon}"
      with open(os.path.join(top_dir,"packages",packageName+".html"),"w") as w:
         w.write("<html>\n")
         w.write("<head>\n")
         w.write("<link rel='stylesheet' href='../fdroid-package.css'>\n")
         w.write("""<meta name="viewport" content="width=device-width, initial-scale=1">\n""")
         w.write(f"<title>{name}</title>\n")
         w.write("</head>\n")
         w.write("<body>\n")
         w.write(f"<img src='{icon}' class='icon'/><h1>{name}</h1>")
         w.write("</body>\n")
         w.write("</html>\n")
if "__main__" == __name__:
   generate_index_html(cfg["INDEX_JSON"],cfg["TOP_DIR"],cfg["MIRROR_DIR"])
   # Not used at this time:
   #generate_app_pages(cfg["INDEX_JSON"],cfg["TOP_DIR"])

I wanted to get an amazing web frontend like f-droid.org has but I think they have more metadata available to them because they actually compile the apps and so have the source and that complex metadata available inside the various possible places. I didn't want to go through all that effort, so I gave up on the idea of an individual page per app.

Comments