Skip to content

Commit fdabb12

Browse files
Add collection info script
1 parent ad04b2a commit fdabb12

2 files changed

Lines changed: 163 additions & 0 deletions

File tree

.github/workflows/deploy.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ jobs:
3737
- name: Generate similar apps
3838
working-directory: similar
3939
run: python main.py --output-folder ../website/similar
40+
41+
- name: Generate Flathub category info
42+
working-directory: similar
43+
run: python category-info.py --output-folder ../website/collection
4044

4145
- name: Debug output
4246
run: |

similar/category-info.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import argparse
2+
import gzip
3+
import json
4+
import time
5+
import sys
6+
import urllib.request
7+
import urllib.error
8+
import urllib.parse
9+
from pathlib import Path
10+
11+
BASE_URL = "https://flathub.org/api/v2"
12+
ADWAITA_URL = "https://arewelibadwaitayet.com/api/apps"
13+
PER_PAGE = 1000
14+
QUALITY_PAGE_SIZE = 1000
15+
REQUEST_DELAY = 0.2
16+
17+
COLLECTIONS = {
18+
"/collection/popular": "popular",
19+
"/collection/trending": "trending",
20+
"/collection/recently-updated": "recently_updated",
21+
"/collection/recently-added": "recently_added",
22+
}
23+
24+
BOOL_FIELDS = ["quality_passing", "kde", "gnome"]
25+
26+
27+
def http_get_json(url: str, params: dict | None = None, timeout: int = 30) -> dict:
28+
if params:
29+
url = f"{url}?{urllib.parse.urlencode(params)}"
30+
req = urllib.request.Request(url, headers={"User-Agent": "Bazaar Site fetcher"})
31+
with urllib.request.urlopen(req, timeout=timeout) as resp:
32+
charset = resp.headers.get_content_charset() or "utf-8"
33+
body = resp.read().decode(charset)
34+
return json.loads(body)
35+
36+
37+
def fetch_collection(path: str) -> list[dict]:
38+
hits = []
39+
page = 1
40+
data = {}
41+
while True:
42+
data = http_get_json(
43+
f"{BASE_URL}{path}",
44+
params={"page": page, "per_page": PER_PAGE},
45+
timeout=30,
46+
)
47+
page_hits = data.get("hits", [])
48+
hits.extend(page_hits)
49+
total_hits = data.get("totalHits", len(hits))
50+
total_pages = data.get("totalPages", page)
51+
print(
52+
f" {path}: page {page}/{total_pages} "
53+
f"({len(hits)}/{total_hits} apps so far)"
54+
)
55+
if page >= total_pages or not page_hits:
56+
break
57+
page += 1
58+
time.sleep(REQUEST_DELAY)
59+
return hits
60+
61+
62+
def fetch_quality_passing_ids() -> set[str]:
63+
ids: set[str] = set()
64+
page = 1
65+
while True:
66+
data = http_get_json(
67+
f"{BASE_URL}/quality-moderation/passing-apps",
68+
params={"page": page, "page_size": QUALITY_PAGE_SIZE},
69+
timeout=30,
70+
)
71+
apps = data.get("apps", [])
72+
ids.update(apps)
73+
print(f" quality-moderation: page {page} ({len(ids)} apps so far)")
74+
if len(apps) < QUALITY_PAGE_SIZE:
75+
break
76+
page += 1
77+
time.sleep(REQUEST_DELAY)
78+
return ids
79+
80+
81+
def fetch_kde_ids() -> set[str]:
82+
hits = fetch_collection("/collection/developer/kde")
83+
return {app.get("app_id") or app.get("id") for app in hits if app.get("app_id") or app.get("id")}
84+
85+
86+
def fetch_gnome_ids() -> set[str]:
87+
data = http_get_json(ADWAITA_URL, timeout=30)
88+
if isinstance(data, dict):
89+
return set(data.keys())
90+
return set()
91+
92+
93+
def build_merged_index() -> dict:
94+
merged: dict[str, dict] = {}
95+
96+
def get_entry(app_id: str) -> dict:
97+
return merged.setdefault(
98+
app_id,
99+
{
100+
**{k: None for k in COLLECTIONS.values()},
101+
**{k: False for k in BOOL_FIELDS},
102+
},
103+
)
104+
105+
for path, key in COLLECTIONS.items():
106+
print(f"Fetching {key} ...")
107+
hits = fetch_collection(path)
108+
for rank, app in enumerate(hits, start=1):
109+
app_id = app.get("app_id") or app.get("id")
110+
if not app_id:
111+
continue
112+
entry = get_entry(app_id)
113+
entry[key] = rank
114+
time.sleep(REQUEST_DELAY)
115+
116+
print("Fetching quality")
117+
for app_id in fetch_quality_passing_ids():
118+
get_entry(app_id)["quality_passing"] = True
119+
time.sleep(REQUEST_DELAY)
120+
121+
print("Fetching KDE apps")
122+
for app_id in fetch_kde_ids():
123+
get_entry(app_id)["kde"] = True
124+
time.sleep(REQUEST_DELAY)
125+
126+
print("Fetching Adwaita")
127+
for app_id in fetch_gnome_ids():
128+
get_entry(app_id)["gnome"] = True
129+
130+
return merged
131+
132+
133+
def parse_args():
134+
parser = argparse.ArgumentParser()
135+
parser.add_argument(
136+
"--output-folder",
137+
default="collection-data",
138+
)
139+
return parser.parse_args()
140+
141+
142+
def main():
143+
args = parse_args()
144+
merged = build_merged_index()
145+
sorted_merged = {app_id: merged[app_id] for app_id in sorted(merged)}
146+
payload = json.dumps(sorted_merged, indent=2, ensure_ascii=False)
147+
out_path = Path(args.output_folder)
148+
out_path.parent.mkdir(parents=True, exist_ok=True)
149+
with gzip.open(out_path, "wt", encoding="utf-8") as f:
150+
f.write(payload)
151+
print(f"\nWrote {len(sorted_merged)} apps to {out_path.resolve()}")
152+
153+
154+
if __name__ == "__main__":
155+
try:
156+
main()
157+
except urllib.error.URLError as e:
158+
print(f"Request failed: {e}", file=sys.stderr)
159+
sys.exit(1)

0 commit comments

Comments
 (0)