diff --git a/README b/README new file mode 100644 index 0000000..1325d73 --- /dev/null +++ b/README @@ -0,0 +1,3 @@ +This is very much currently WIP. Right now it only fetches data from appstream. + +sqlite database is temporary. no need to store things when they are already stored in appstream to begin with diff --git a/flatshop_db b/flatshop_db new file mode 100644 index 0000000..68ae7a3 Binary files /dev/null and b/flatshop_db differ diff --git a/libflatpak_query.py b/libflatpak_query.py new file mode 100755 index 0000000..3666f2b --- /dev/null +++ b/libflatpak_query.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +import gi +gi.require_version("AppStream", "1.0") +gi.require_version("Flatpak", "1.0") + +from gi.repository import Flatpak, GLib, Gio, AppStream +from pathlib import Path +import logging +from enum import IntEnum +from pathlib import Path +import argparse +import sys + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class Match(IntEnum): + NAME = 1 + ID = 2 + SUMMARY = 3 + NONE = 4 + + +class AppStreamPackage: + def __init__(self, comp: AppStream.Component, remote: Flatpak.Remote) -> None: + self.component: AppStream.Component = comp + self.remote: Flatpak.Remote = remote + self.repo_name: str = remote.get_name() + bundle: AppStream.Bundle = comp.get_bundle(AppStream.BundleKind.FLATPAK) + self.flatpak_bundle: str = bundle.get_id() + self.match = Match.NONE + + # Get icon and description + self.icon_url = self._get_icon_url() + self.icon_path_128 = self._get_icon_cache_path("128x128") + self.icon_path_64 = self._get_icon_cache_path("64x64") + self.icon_filename = self._get_icon_filename() + self.description = self.component.get_description() + + # Get URLs from the component + self.urls = self._get_urls() + + self.developer = self.component.get_developer().get_name() + self.categories = self._get_categories() + + @property + def id(self) -> str: + return self.component.get_id() + + @property + def name(self) -> str: + return self.component.get_name() + + @property + def summary(self) -> str: + return self.component.get_summary() + + @property + def version(self) -> str: + releases = self.component.get_releases_plain() + if releases: + release = releases.index_safe(0) + if release: + version = release.get_version() + return version + return None + + def _get_icon_url(self) -> str: + """Get the remote icon URL from the component""" + icons = self.component.get_icons() + + # Find the first REMOTE icon + remote_icon = next((icon for icon in icons if icon.get_kind() == AppStream.IconKind.REMOTE), None) + return remote_icon.get_url() if remote_icon else "" + + def _get_icon_filename(self) -> str: + """Get the cached icon filename from the component""" + icons = self.component.get_icons() + + # Find the first CACHED icon + cached_icon = next((icon for icon in icons if icon.get_kind() == AppStream.IconKind.CACHED), None) + return cached_icon.get_filename() if cached_icon else "" + + def _get_icon_cache_path(self, size: str) -> str: + + # Remove the file:// prefix + icon_filename = self._get_icon_filename() + + # Appstream icon cache path for the flatpak repo queried + icon_cache_path = Path(self.remote.get_appstream_dir().get_path() + "/icons/flatpak/" + size + "/") + return str(icon_cache_path) + + def _get_urls(self) -> dict: + """Get URLs from the component""" + urls = { + 'donation': self._get_url('donation'), + 'homepage': self._get_url('homepage'), + 'bugtracker': self._get_url('bugtracker') + } + return urls + + def _get_url(self, url_kind: str) -> str: + """Helper method to get a specific URL type""" + # Convert string to AppStream.UrlKind enum + url_kind_enum = getattr(AppStream.UrlKind, url_kind.upper()) + url = self.component.get_url(url_kind_enum) + if url: + return url + return "" + + def _get_categories(self) -> list: + categories_fetch = self.component.get_categories() + categories = [] + for category in categories_fetch: + categories.append(category.lower()) + return categories + + def search(self, keyword: str) -> Match: + """Search for keyword in package details""" + if keyword in self.name.lower(): + return Match.NAME + elif keyword in self.id.lower(): + return Match.ID + elif keyword in self.summary.lower(): + return Match.SUMMARY + else: + return Match.NONE + + def __str__(self) -> str: + return f"{self.name} - {self.summary} ({self.flatpak_bundle})" + + def get_details(self) -> dict: + """Get all package details including icon and description""" + return { + "name": self.name, + "id": self.id, + "summary": self.summary, + "description": self.description, + "version": self.version, + "icon_url": self.icon_url, + "icon_path_128": self.icon_path_128, + "icon_path_64": self.icon_path_64, + "icon_filename": self.icon_filename, + "urls": self.urls, + "developer": self.developer, + #"architectures": self.architectures, + "categories": self.categories, + "bundle_id": self.flatpak_bundle, + "match_type": self.match.name, + "repo": self.repo_name + } + +class AppstreamSearcher: + """Flatpak AppStream Package seacher""" + + def __init__(self) -> None: + self.remotes: dict[str, list[AppStreamPackage]] = {} + self.installed = [] + + def add_installation(self, inst: Flatpak.Installation): + """Add enabled flatpak repositories from Flatpak.Installation""" + remotes = inst.list_remotes() + for remote in remotes: + if not remote.get_disabled(): + self.add_remote(remote, inst) + + def add_remote(self, remote: Flatpak.Remote, inst: Flatpak.Installation): + """Add packages for a given Flatpak.Remote""" + remote_name = remote.get_name() + self.installed.extend([ref.format_ref() for ref in inst.list_installed_refs_by_kind(Flatpak.RefKind.APP)]) + if remote_name not in self.remotes: + self.remotes[remote_name] = self._load_appstream_metadata(remote) + + def _load_appstream_metadata(self, remote: Flatpak.Remote) -> list[AppStreamPackage]: + """load AppStrean metadata and create AppStreamPackage objects""" + packages = [] + metadata = AppStream.Metadata.new() + metadata.set_format_style(AppStream.FormatStyle.CATALOG) + appstream_file = Path(remote.get_appstream_dir().get_path() + "/appstream.xml.gz") + if appstream_file.exists(): + metadata.parse_file(Gio.File.new_for_path(appstream_file.as_posix()), AppStream.FormatKind.XML) + components: AppStream.ComponentBox = metadata.get_components() + i = 0 + for i in range(components.get_size()): + component = components.index_safe(i) + if component.get_kind() == AppStream.ComponentKind.DESKTOP_APP: + bundle = component.get_bundle(AppStream.BundleKind.FLATPAK).get_id() + if bundle not in self.installed: + packages.append(AppStreamPackage(component, remote)) + return packages + else: + logger.debug(f"AppStream file not found: {appstream_file}") + return [] + + def search_flatpak_repo(self, keyword: str, repo_name: str) -> list[AppStreamPackage]: + search_results = [] + packages = self.remotes[repo_name] + for package in packages: + found = package.search(keyword) + if found != Match.NONE: + logger.debug(f" found : {package} match: {found}") + package.match = found + search_results.append(package) + return search_results + + + def search_flatpak(self, keyword: str, repo_name=None) -> list[AppStreamPackage]: + """Search packages matching a keyword""" + search_results = [] + keyword = keyword.lower() + if repo_name: + search_results = self.search_flatpak_repo(keyword, repo_name) + else: + for remote_name in self.remotes.keys(): + results = self.search_flatpak_repo(keyword, remote_name) + for result in results: + search_results.append(result) + return search_results + +def main(): + """Main function demonstrating Flatpak information retrieval""" + + parser = argparse.ArgumentParser(description='Search Flatpak packages') + parser.add_argument('--id', help='Application ID to search for') + parser.add_argument('--repo', help='Filter results to specific repository') + + args = parser.parse_args() + app_id = args.id + repo_filter = args.repo + + if not app_id: + print("Usage: python flatpak_info.py --