From 4c3f6e06b0afa93b4ce0e1afd967a72bb80f9a6f Mon Sep 17 00:00:00 2001 From: Kolja Lampe Date: Thu, 9 Jan 2025 10:16:33 +0100 Subject: [PATCH] Add mobile apps listing to backend --- backend/app/app.py | 18 +++++++++++++ backend/app/apps.py | 1 + backend/app/search.py | 20 +++++++++++++++ backend/app/utils.py | 60 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/backend/app/app.py b/backend/app/app.py index 897f7c42c..0a02d0f4f 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -281,6 +281,24 @@ def get_verified( return result +@router.get("/collection/mobile", tags=["app"]) +def get_mobile( + page: int | None = None, + per_page: int | None = None, + locale: str = "en", + response: Response = Response(), +): + if (page is None and per_page is not None) or ( + page is not None and per_page is None + ): + response.status_code = 400 + return response + + result = search.get_by_mobile(page, per_page, locale) + + return result + + @router.get("/popular/last-month", tags=["app"]) def get_popular_last_month( page: int | None = None, diff --git a/backend/app/apps.py b/backend/app/apps.py index cbee4547c..998d6ea0c 100644 --- a/backend/app/apps.py +++ b/backend/app/apps.py @@ -67,6 +67,7 @@ def add_to_search(app_id: str, app: dict, apps_locale: dict) -> dict: "id": utils.get_clean_app_id(app_id), "type": type, "name": app["name"], + "isMobileFriendly": app.get("isMobileFriendly", False), "summary": app["summary"], "translations": translations, "keywords": search_keywords, diff --git a/backend/app/search.py b/backend/app/search.py index 0404bd814..432ea7d96 100644 --- a/backend/app/search.py +++ b/backend/app/search.py @@ -45,6 +45,7 @@ class SearchQuery(BaseModel): "arches", "icon", "keywords", + "isMobileFriendly", ] ) @@ -240,6 +241,25 @@ def get_by_verified(page: int | None, hits_per_page: int | None, locale: str): ) +def get_by_mobile(page: int | None, hits_per_page: int | None, locale: str): + return _translate_name_and_summary( + locale, + client.index("apps").search( + "", + { + "filter": [ + "isMobileFriendly = true", + "type IN [console-application, desktop-application]", + "NOT icon IS NULL", + ], + "sort": ["trending:desc"], + "hitsPerPage": hits_per_page or 250, + "page": page or 1, + }, + ), + ) + + def get_by_developer( developer: str, page: int | None, hits_per_page: int | None, locale: str ): diff --git a/backend/app/utils.py b/backend/app/utils.py index c94d29e7e..25bbd3cbd 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -5,7 +5,7 @@ import os import re from datetime import datetime, timedelta -from typing import Any +from typing import Any, Literal import gi import jwt @@ -21,6 +21,9 @@ clean_id_re = re.compile("[^a-zA-Z0-9_-]+") remove_desktop_re = re.compile(r"\.desktop$") +mobile_min_size = 360 +mobile_max_size = 768 + class Hasher: """ @@ -96,6 +99,46 @@ def appstream2dict(appstream_url=None) -> dict[str, dict]: app["type"] = component.attrib.get("type", "generic") app["locales"] = {} + isMobileFriendly: list[bool] = [] + requires = component.find("requires") + if requires is not None: + if display_lengths := requires.findall("display_length"): + for display_length in display_lengths: + compare: Literal[ + "eq", + "ne", + "lt", + "gt", + "le", + "ge", + ] = display_length.attrib.get("compare") or "ge" + + isMobileFriendly.append( + compare_requires_is_between_mobile_target( + int(display_length.text), + compare, + ) + ) + + hasTouch = False + supports = component.find("supports") + if supports is not None: + if controls := supports.findall("control"): + for control in controls: + if control.text == "touch": + hasTouch = True + + recommends = component.find("recommends") + if recommends is not None: + if controls := recommends.findall("control"): + for control in controls: + if control.text == "touch": + hasTouch = True + + app["isMobileFriendly"] = ( + all(isMobileFriendly) and any(isMobileFriendly) and hasTouch + ) + descriptions = component.findall("description") if len(descriptions): for desc in descriptions: @@ -429,6 +472,21 @@ def appstream2dict(appstream_url=None) -> dict[str, dict]: return apps +def compare_requires_is_between_mobile_target( + display_length: int, + compare: Literal["ge", "lt", "gt", "le", "eq", "ne"], +): + if compare == "ge" and display_length <= mobile_max_size: + return True + if compare == "lt" and display_length > mobile_max_size: + return True + if compare == "gt" and display_length < mobile_min_size: + return True + if compare == "le" and display_length >= mobile_min_size: + return True + return False + + def get_clean_app_id(app_id: str): return re.sub(clean_id_re, "_", app_id)