[gnome-software/mwleeds/pwa-plugin: 233/234] Add pwa-metainfo-generator




commit 764e27405cd08d5cf5198cd5a28309d20e2cb077
Author: Phaedrus Leeds <mwleeds protonmail com>
Date:   Wed Dec 22 17:18:23 2021 -0800

    Add pwa-metainfo-generator
    
    This is a Python script for generating AppStream metainfo for a set of
    web apps given as input, so that GNOME Software can display them as
    installable. The intended user is distributions that want to include web
    apps in their set of available software, or potentially GNOME if we
    decide to include some by default
    (https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1575)

 .../gnome-software-foss-pwa-list.csv               |   9 ++
 .../gnome-software-foss-pwa-list.xml               | 180 +++++++++++++++++++++
 pwa-metainfo-generator/pwa-metainfo-generator.py   | 160 ++++++++++++++++++
 3 files changed, 349 insertions(+)
---
diff --git a/pwa-metainfo-generator/gnome-software-foss-pwa-list.csv 
b/pwa-metainfo-generator/gnome-software-foss-pwa-list.csv
new file mode 100644
index 000000000..140e12c2e
--- /dev/null
+++ b/pwa-metainfo-generator/gnome-software-foss-pwa-list.csv
@@ -0,0 +1,9 @@
+https://app.diagrams.net/,Apache-2.0
+https://pinafore.social/,AGPL-3.0-only
+https://devdocs.io/,MPL-2.0
+https://snapdrop.net/,GPL-3.0-only
+https://stackedit.io/app,Apache-2.0
+https://squoosh.app/,Apache-2.0
+https://excalidraw.com/,MIT
+https://crypt.ee/,MIT
+https://web.5217.app/,GPL-2.0-only
diff --git a/pwa-metainfo-generator/gnome-software-foss-pwa-list.xml 
b/pwa-metainfo-generator/gnome-software-foss-pwa-list.xml
new file mode 100644
index 000000000..a491faa24
--- /dev/null
+++ b/pwa-metainfo-generator/gnome-software-foss-pwa-list.xml
@@ -0,0 +1,180 @@
+<?xml version='1.0' encoding='utf-8'?>
+<components version="0.15">
+  <component type="webapp">
+    <launchable type="url">https://app.diagrams.net/</launchable>
+    <url type="homepage">https://app.diagrams.net/</url>
+    <project_license>Apache-2.0</project_license>
+    <metadata_license>FSFAP</metadata_license>
+    <name>Diagrams</name>
+    <id>org.gnome.Software.WebApp-527a2dd6729c3574227c145bbc447997f0048537</id>
+    <icon type="remote" width="512" 
height="512">https://app.diagrams.net/images/android-chrome-512x512.png</icon>
+    <summary>diagrams.net is a completely free diagram editor</summary>
+  </component>
+  <component type="webapp">
+    <launchable type="url">https://pinafore.social/</launchable>
+    <url type="homepage">https://pinafore.social/</url>
+    <project_license>AGPL-3.0-only</project_license>
+    <metadata_license>FSFAP</metadata_license>
+    <name>Pinafore</name>
+    <id>org.gnome.Software.WebApp-e636aa5f2069f6e9c02deccc7b65f43da7985e32</id>
+    <icon type="remote" width="48" height="48">https://pinafore.social/icon-48.png</icon>
+    <icon type="remote" width="72" height="72">https://pinafore.social/icon-72.png</icon>
+    <icon type="remote" width="96" height="96">https://pinafore.social/icon-96.png</icon>
+    <icon type="remote" width="144" height="144">https://pinafore.social/icon-144.png</icon>
+    <icon type="remote" width="192" height="192">https://pinafore.social/icon-192.png</icon>
+    <icon type="remote" width="512" height="512">https://pinafore.social/icon-512.png</icon>
+    <icon type="remote" width="44" height="44">https://pinafore.social/icon-44.png</icon>
+    <icon type="remote" width="50" height="50">https://pinafore.social/icon-50.png</icon>
+    <icon type="remote" width="150" height="150">https://pinafore.social/icon-150.png</icon>
+    <screenshots>
+      <screenshot type="default">
+        <image width="540" height="720">https://pinafore.social/screenshot-540-720-1.png</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="540" height="720">https://pinafore.social/screenshot-540-720-2.png</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="540" height="720">https://pinafore.social/screenshot-540-720-3.png</image>
+      </screenshot>
+    </screenshots>
+    <categories>
+      <category>Chat</category>
+    </categories>
+    <summary>An alternative web client for Mastodon, focused on speed and simplicity.</summary>
+  </component>
+  <component type="webapp">
+    <launchable type="url">https://devdocs.io/</launchable>
+    <url type="homepage">https://devdocs.io/</url>
+    <project_license>MPL-2.0</project_license>
+    <metadata_license>FSFAP</metadata_license>
+    <name>DevDocs</name>
+    <id>org.gnome.Software.WebApp-962f69328d6450369f25a6fdb6bbd1764d11a3c8</id>
+    <icon type="remote" width="32" height="32">https://devdocs.io/images/webapp-icon-32.png</icon>
+    <icon type="remote" width="60" height="60">https://devdocs.io/images/webapp-icon-60.png</icon>
+    <icon type="remote" width="80" height="80">https://devdocs.io/images/webapp-icon-80.png</icon>
+    <icon type="remote" width="128" height="128">https://devdocs.io/images/webapp-icon-128.png</icon>
+    <icon type="remote" width="192" height="192">https://devdocs.io/images/webapp-icon-192.png</icon>
+    <icon type="remote" width="256" height="256">https://devdocs.io/images/webapp-icon-256.png</icon>
+    <icon type="remote" width="512" height="512">https://devdocs.io/images/webapp-icon-512.png</icon>
+    <summary>API Documentation Browser</summary>
+  </component>
+  <component type="webapp">
+    <launchable type="url">https://snapdrop.net/</launchable>
+    <url type="homepage">https://snapdrop.net/</url>
+    <project_license>GPL-3.0-only</project_license>
+    <metadata_license>FSFAP</metadata_license>
+    <name>Snapdrop</name>
+    <id>org.gnome.Software.WebApp-4dda9043da5b2d2aa57c6064c907c1ae3fe58941</id>
+    <icon type="remote" width="192" 
height="192">https://snapdrop.net/images/android-chrome-192x192.png</icon>
+    <icon type="remote" width="512" 
height="512">https://snapdrop.net/images/android-chrome-512x512.png</icon>
+    <icon type="remote" width="96" height="96">https://snapdrop.net/images/favicon-96x96.png</icon>
+  </component>
+  <component type="webapp">
+    <launchable type="url">https://stackedit.io/app</launchable>
+    <url type="homepage">https://stackedit.io/app</url>
+    <project_license>Apache-2.0</project_license>
+    <metadata_license>FSFAP</metadata_license>
+    <name>StackEdit</name>
+    <id>org.gnome.Software.WebApp-e7aada0261449b5239c5c458c48fe92717950d1a</id>
+    <icon type="remote" width="512" 
height="512">https://stackedit.io/icon_512x512.bc1452c3b77b59eb0d8c449de1c77310.png</icon>
+    <icon type="remote" width="384" 
height="384">https://stackedit.io/icon_384x384.3f55b77300c8b37b4aa380c8460ee0a2.png</icon>
+    <icon type="remote" width="256" 
height="256">https://stackedit.io/icon_256x256.f406288343ae34c5c7bd1543700c2ac0.png</icon>
+    <icon type="remote" width="192" 
height="192">https://stackedit.io/icon_192x192.eec0feaecc5bef1a499b3de11bde6b27.png</icon>
+    <icon type="remote" width="128" 
height="128">https://stackedit.io/icon_128x128.43ad56ce3535a3ffe333a76a05c13267.png</icon>
+    <icon type="remote" width="96" 
height="96">https://stackedit.io/icon_96x96.1d93fc90dc62ceec6c921786ddab3f69.png</icon>
+    <summary>Full-featured, open-source Markdown editor</summary>
+  </component>
+  <component type="webapp">
+    <launchable type="url">https://squoosh.app/</launchable>
+    <url type="homepage">https://squoosh.app/</url>
+    <project_license>Apache-2.0</project_license>
+    <metadata_license>FSFAP</metadata_license>
+    <name>Squoosh</name>
+    <id>org.gnome.Software.WebApp-e41134be0ce95da0832878faa90cf3473b1b52d7</id>
+    <icon type="remote" width="1024" height="1024">https://squoosh.app/c/icon-large-cb438cac.png</icon>
+    <screenshots>
+      <screenshot type="default">
+        <image width="540" height="720">https://squoosh.app/c/screenshot1-0ff68546.png</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="540" height="720">https://squoosh.app/c/screenshot2-1f78c4db.jpg</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="540" height="720">https://squoosh.app/c/screenshot3-c1e02216.jpg</image>
+      </screenshot>
+    </screenshots>
+    <categories>
+      <category>Photography</category>
+      <category>Office</category>
+      <category>Utility</category>
+    </categories>
+    <summary>Compress and compare images with different codecs, right in your browser.</summary>
+  </component>
+  <component type="webapp">
+    <launchable type="url">https://excalidraw.com/</launchable>
+    <url type="homepage">https://excalidraw.com/</url>
+    <project_license>MIT</project_license>
+    <metadata_license>FSFAP</metadata_license>
+    <name>Excalidraw</name>
+    <id>org.gnome.Software.WebApp-ea52ad18346faac27072cd1654243dfb7c70ef14</id>
+    <icon type="remote" width="180" height="180">https://excalidraw.com/logo-180x180.png</icon>
+    <icon type="remote" width="256" height="256">https://excalidraw.com/apple-touch-icon.png</icon>
+    <screenshots>
+      <screenshot type="default">
+        <image width="462" height="945">https://excalidraw.com/screenshots/virtual-whiteboard.png</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="462" height="945">https://excalidraw.com/screenshots/wireframe.png</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="462" height="945">https://excalidraw.com/screenshots/illustration.png</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="462" height="945">https://excalidraw.com/screenshots/shapes.png</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="462" height="945">https://excalidraw.com/screenshots/collaboration.png</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="462" height="945">https://excalidraw.com/screenshots/export.png</image>
+      </screenshot>
+    </screenshots>
+    <summary>Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn 
feel to them.</summary>
+  </component>
+  <component type="webapp">
+    <launchable type="url">https://crypt.ee/</launchable>
+    <url type="homepage">https://crypt.ee/</url>
+    <project_license>MIT</project_license>
+    <metadata_license>FSFAP</metadata_license>
+    <name>Cryptee</name>
+    <id>org.gnome.Software.WebApp-c247cd2b007a4b2ac9ff2f371cd2d8eb67037e94</id>
+    <icon type="remote" width="192" height="192">https://crypt.ee/assets/masked-logo.png</icon>
+    <icon type="remote" width="512" height="512">https://crypt.ee/assets/masked-logo.png</icon>
+    <screenshots>
+      <screenshot type="default">
+        <image width="1792" height="896">https://crypt.ee/imgs/hero/cryptee-photos-docs-hero.jpg</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="1792" height="896">https://crypt.ee/imgs/hero/cryptee-docs-grid.jpg</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="1792" height="896">https://crypt.ee/imgs/hero/cryptee-photos-2.jpg</image>
+      </screenshot>
+      <screenshot type="default">
+        <image width="1792" height="896">https://crypt.ee/imgs/hero/cryptee-docs-duo.jpg</image>
+      </screenshot>
+    </screenshots>
+    <summary>Cryptee is a safe, encrypted place to write personal documents, store photos privately and 
more.</summary>
+  </component>
+  <component type="webapp">
+    <launchable type="url">https://web.5217.app/</launchable>
+    <url type="homepage">https://web.5217.app/</url>
+    <project_license>GPL-2.0-only</project_license>
+    <metadata_license>FSFAP</metadata_license>
+    <name>5217</name>
+    <id>org.gnome.Software.WebApp-d58c3915ccabebc0df844db7ee15c6a4d6eada64</id>
+    <icon type="remote" width="192" 
height="192">https://web.5217.app/favicon/android-chrome-192x192.png</icon>
+    <icon type="remote" width="256" 
height="256">https://web.5217.app/favicon/android-chrome-256x256.png</icon>
+    <icon type="remote" width="512" 
height="512">https://web.5217.app/favicon/android-chrome-512x512.png</icon>
+  </component>
+</components>
\ No newline at end of file
diff --git a/pwa-metainfo-generator/pwa-metainfo-generator.py 
b/pwa-metainfo-generator/pwa-metainfo-generator.py
new file mode 100755
index 000000000..a0fd66b9b
--- /dev/null
+++ b/pwa-metainfo-generator/pwa-metainfo-generator.py
@@ -0,0 +1,160 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright 2021 Matthew Leeds
+# SPDX-License-Identifier: GPL-2.0+
+
+"""
+Generates AppStream metainfo for a set of Progressive Web Apps
+
+Usage: pwa-metainfo-generator.py LIST.CSV
+
+The CSV format expected looks like this:
+
+https://app.diagrams.net/,Apache-2.0
+https://pinafore.social/,AGPL-3.0-only
+...
+
+The output will be written to a file with the same name as the input but a .xml
+file ending.
+
+This tool uses the web app's manifest to fill out the AppStream info, so an
+Internet connection is required
+"""
+
+import csv
+import sys
+import xml.etree.ElementTree as ET
+import requests
+import json
+import hashlib
+from urllib.parse import urljoin
+from bs4 import BeautifulSoup
+
+# w3c categories: https://github.com/w3c/manifest/wiki/Categories
+# appstream categories: https://specifications.freedesktop.org/menu-spec/latest/apa.html
+# left out due to no corresponding appstream category:
+# entertainment, food, government, kids, lifestyle, personalization, politics, shopping, travel
+w3c_to_appstream_categories = {
+    "books": "Literature",
+    "business": "Office",
+    "education": "Education",
+    "finance": "Finance",
+    "fitness": "Sports",
+    "games": "Game",
+    "health": "MedicalSoftware",
+    "magazines": "News",
+    "medical": "MedicalSoftware",
+    "music": "Music",
+    "navigation": "Maps",
+    "news": "News",
+    "photo": "Photography",
+    "productivity": "Office",
+    "security": "Security",
+    "social": "Chat",
+    "sports": "Sports",
+    "utilities": "Utility",
+    "weather": "Utility"
+}
+
+
+def get_manifest_for_url(url):
+    response = requests.get(url)
+    response.raise_for_status()
+    soup = BeautifulSoup(response.text, 'html.parser')
+    manifest_path = soup.head.find('link', rel='manifest', href=True)['href']
+    manifest_response = requests.get(urljoin(url, manifest_path))
+    manifest_response.raise_for_status()
+    return json.loads(manifest_response.text)
+
+def copy_metainfo_from_manifest(url, app_component, manifest):
+    # Short name seems more suitable in practice
+    try:
+      ET.SubElement(app_component, 'name').text = manifest['short_name']
+    except KeyError:
+      ET.SubElement(app_component, 'name').text = manifest['name']
+
+    # Generate a unique app ID that meets the spec requirements. A different
+    # app ID will be used upon install that is determined by the backing browser
+    app_id = 'org.gnome.Software.WebApp-' + hashlib.sha1(url.encode('utf-8')).hexdigest()
+    ET.SubElement(app_component, 'id').text = app_id
+
+    # Avoid using maskable icons if we can, they don't have nice rounded edges
+    normal_icon_exists = False
+    for icon in manifest['icons']:
+        if 'purpose' not in icon or icon['purpose'] == 'any':
+            normal_icon_exists = True
+
+    for icon in manifest['icons']:
+        if 'purpose' in icon and icon['purpose'] != 'any' and normal_icon_exists:
+            continue
+        icon_element = ET.SubElement(app_component, 'icon')
+        icon_element.text = urljoin(url, icon['src'])
+        icon_element.set('type', 'remote')
+        size = icon['sizes'].split(' ')[-1]
+        icon_element.set('width', size.split('x')[0])
+        icon_element.set('height', size.split('x')[1])
+
+    if 'screenshots' in manifest:
+        screenshots_element = ET.SubElement(app_component, 'screenshots')
+        for screenshot in manifest['screenshots']:
+            screenshot_element = ET.SubElement(screenshots_element, 'screenshot')
+            screenshot_element.set('type', 'default')
+            image_element = ET.SubElement(screenshot_element, 'image')
+            image_element.text = urljoin(url, screenshot['src'])
+            size = screenshot['sizes'].split(' ')[-1]
+            image_element.set('width', size.split('x')[0])
+            image_element.set('height', size.split('x')[1])
+            if 'label' in screenshot:
+                ET.SubElement(screenshot_element, 'caption').text = screenshot['label']
+
+    if 'categories' in manifest:
+        categories_element = ET.SubElement(app_component, 'categories')
+        for category in manifest['categories']:
+            try:
+                mapped_category = w3c_to_appstream_categories[category]
+                ET.SubElement(categories_element, 'category').text = mapped_category
+            except KeyError:
+                pass
+
+    if 'description' in manifest:
+        ET.SubElement(app_component, 'summary').text = manifest['description']
+
+def main():
+    if len(sys.argv) != 2 or not sys.argv[1].endswith('.csv'):
+        print('Usage: {} input.csv'.format(sys.argv[0]))
+        sys.exit(1)
+
+    input_filename = sys.argv[1]
+    out_filename = input_filename.replace('.csv', '.xml')
+    with open(input_filename) as input_csv:
+        components = ET.Element('components')
+        components.set('version', '0.15')
+        reader = csv.reader(input_csv)
+        for app in reader:
+            app_component = ET.SubElement(components, 'component')
+            app_component.set('type', 'webapp')
+
+            launchable = ET.SubElement(app_component, 'launchable')
+            launchable.set('type', 'url')
+            launchable.text = app[0]
+
+            url = ET.SubElement(app_component, 'url')
+            url.set('type', 'homepage')
+            url.text = app[0]
+
+            project_license = ET.SubElement(app_component, 'project_license')
+            project_license.text = app[1]
+
+            # metadata license is a required field but we don't have one, assume FSFAP?
+            metadata_license = ET.SubElement(app_component, 'metadata_license')
+            metadata_license.text = 'FSFAP'
+
+            copy_metainfo_from_manifest(app[0], app_component, get_manifest_for_url(app[0]))
+
+    tree = ET.ElementTree(components)
+    ET.indent(tree)
+    tree.write(out_filename, xml_declaration=True, encoding='utf-8', method='xml')
+
+
+if __name__=='__main__':
+        main()


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]