#!/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 --