From c29b1a02527ee7846a9689f50122093a54eb3737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Moreira?= Date: Mon, 23 Dec 2019 13:04:51 -0300 Subject: [PATCH] suggestions button implemented respecting already installed applications --- CHANGELOG.md | 1 + bauh/api/http.py | 2 +- bauh/gems/appimage/controller.py | 24 +++- bauh/gems/appimage/query.py | 1 + bauh/gems/arch/controller.py | 5 +- bauh/gems/flatpak/controller.py | 16 ++- bauh/gems/flatpak/flatpak.py | 1 + bauh/gems/snap/controller.py | 27 ++-- bauh/gems/web/controller.py | 11 ++ bauh/view/core/controller.py | 6 +- bauh/view/qt/thread.py | 3 +- bauh/view/qt/window.py | 32 ++++- bauh/view/resources/img/suggestions.svg | 168 ++++++++++++++++++++++++ 13 files changed, 270 insertions(+), 27 deletions(-) create mode 100644 bauh/view/resources/img/suggestions.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b6e18c..a949fee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - bauh relies on [NodeJS](https://nodejs.org/en/), [Electron](https://electronjs.org/) and [nativefier](https://github.com/jiahaog/nativefier) to install the Web applications, but there is no need to have them installed on your system. Bauh will create its own installation environment with these technologies in **~/.local/share/bauh/web/env**. - suggestions are retrieved from [suggestions.txt](https://github.com/vinifmor/bauh-files/blob/master/web/suggestions.yml) - requires only **python-beautifulsoup4** and **python-lxml** to be enabled +- **Suggestions** button: it allows retrieving the application suggestions any time ### Improvements - configuration file **~/.config/bauh/config.json** renamed to **~/.config/bauh/config.yml** diff --git a/bauh/api/http.py b/bauh/api/http.py index e025b7b9..64205c4d 100644 --- a/bauh/api/http.py +++ b/bauh/api/http.py @@ -17,7 +17,7 @@ class HttpClient: self.sleep = sleep self.logger = logger - def get(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False): + def get(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False) -> requests.Request: cur_attempts = 1 while cur_attempts <= self.max_attempts: diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 4250a800..fd9ace98 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -26,6 +26,7 @@ from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, r from bauh.gems.appimage import query, INSTALLATION_PATH, LOCAL_PATH, SUGGESTIONS_FILE from bauh.gems.appimage.config import read_config from bauh.gems.appimage.model import AppImage +from bauh.gems.appimage.query import FIND_APPS_BY_NAME, FIND_APPS_BY_NAME_ONLY_NAME from bauh.gems.appimage.worker import DatabaseUpdater DB_APPS_PATH = '{}/{}'.format(HOME_PATH, '.local/share/bauh/appimage/apps.db') @@ -104,7 +105,8 @@ class AppImageManager(SoftwareManager): res.total = len(res.installed) + len(res.new) return res - def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None) -> SearchResult: + def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, + pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None, connection: sqlite3.Connection = None) -> SearchResult: res = SearchResult([], [], 0) if os.path.exists(INSTALLATION_PATH): @@ -122,7 +124,7 @@ class AppImageManager(SoftwareManager): names.add("'{}'".format(app.name.lower())) if res.installed: - con = self._get_db_connection(DB_APPS_PATH) + con = self._get_db_connection(DB_APPS_PATH) if not connection else connection if con: try: @@ -142,7 +144,8 @@ class AppImageManager(SoftwareManager): except: traceback.print_exc() finally: - self._close_connection(DB_APPS_PATH, con) + if not connection: + self._close_connection(DB_APPS_PATH, con) res.total = len(res.installed) return res @@ -432,14 +435,23 @@ class AppImageManager(SoftwareManager): try: sugs = [l for l in file.text.split('\n') if l] - if 0 < limit < len(sugs): - sugs = sugs[0:limit] + if filter_installed: + installed = {i.name.lower() for i in self.read_installed(disk_loader=None, connection=connection).installed} + else: + installed = None sugs_map = {} for s in sugs: lsplit = s.split('=') - sugs_map[lsplit[1].strip()] = SuggestionPriority(int(lsplit[0])) + + name = lsplit[1].strip() + + if limit <= 0 or len(sugs_map) < limit: + if not installed or not name.lower() in installed: + sugs_map[name] = SuggestionPriority(int(lsplit[0])) + else: + break cursor = connection.cursor() cursor.execute(query.FIND_APPS_BY_NAME_FULL.format(','.join(["'{}'".format(s) for s in sugs_map.keys()]))) diff --git a/bauh/gems/appimage/query.py b/bauh/gems/appimage/query.py index 57fe0248..e200e09f 100644 --- a/bauh/gems/appimage/query.py +++ b/bauh/gems/appimage/query.py @@ -5,5 +5,6 @@ RELEASE_ATTRS = ('version', 'url_download', 'published_at') SEARCH_APPS_BY_NAME_OR_DESCRIPTION = "SELECT {} FROM apps".format(','.join(APP_ATTRS)) + " WHERE lower(name) LIKE '%{}%' or lower(description) LIKE '%{}%'" FIND_APP_ID_BY_NAME_AND_GITHUB = "SELECT id FROM apps WHERE lower(name) = '{}' and lower(github) = '{}'" FIND_APPS_BY_NAME = "SELECT name, github, version, url_download FROM apps WHERE lower(name) IN ({})" +FIND_APPS_BY_NAME_ONLY_NAME = "SELECT name FROM apps WHERE lower(name) IN ({})" FIND_APPS_BY_NAME_FULL = "SELECT {} FROM apps".format(','.join(APP_ATTRS)) + " WHERE lower(name) IN ({})" FIND_RELEASES_BY_APP_ID = "SELECT {} FROM releases".format(','.join(RELEASE_ATTRS)) + " WHERE app_id = {} ORDER BY version desc" diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 05857d71..e2de8b8d 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -848,7 +848,10 @@ class ArchManager(SoftwareManager): if l: if limit <= 0 or len(suggestions) < limit: lsplit = l.split('=') - suggestions[lsplit[1].strip()] = SuggestionPriority(int(lsplit[0])) + name = lsplit[1].strip() + + if not filter_installed or not pacman.check_installed(name): + suggestions[name] = SuggestionPriority(int(lsplit[0])) api_res = self.aur_client.get_info(suggestions.keys()) diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 35980742..f4a367aa 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -147,7 +147,12 @@ class FlatpakManager(SoftwareManager): return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.update(pkg.ref))) def uninstall(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool: - return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref))) + uninstalled = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref))) + + if self.suggestions_cache: + self.suggestions_cache.delete(pkg.id) + + return uninstalled def get_info(self, app: FlatpakApplication) -> dict: if app.installed: @@ -272,12 +277,19 @@ class FlatpakManager(SoftwareManager): return res else: self.logger.info("Mapping suggestions") + installed = {i.id for i in self.read_installed(disk_loader=None).installed} if filter_installed else None for line in file.text.split('\n'): if line: if limit <= 0 or len(res) < limit: sug = line.split('=') - appid, priority = sug[1], SuggestionPriority(int(sug[0])) + appid = sug[1].strip() + + if installed and appid in installed: + continue + + priority = SuggestionPriority(int(sug[0])) + cached_sug = self.suggestions_cache.get(appid) if cached_sug: diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index 10791db4..ba9d40a4 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -8,6 +8,7 @@ from bauh.api.exception import NoInternetException from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess BASE_CMD = 'flatpak' +RE_SEVERAL_SPACES = re.compile(r'\s+') def get_app_info_fields(app_id: str, branch: str, fields: List[str] = [], check_runtime: bool = False): diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 47608311..183b2ab0 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -126,7 +126,12 @@ class SnapManager(SoftwareManager): raise Exception("'update' is not supported by {}".format(pkg.__class__.__name__)) def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: - return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.uninstall_and_stream(pkg.name, root_password))) + uninstalled = ProcessHandler(watcher).handle(SystemProcess(subproc=snap.uninstall_and_stream(pkg.name, root_password))) + + if self.suggestions_cache: + self.suggestions_cache.delete(pkg.name) + + return uninstalled def get_managed_types(self) -> Set[Type[SoftwarePackage]]: return {SnapApplication} @@ -250,20 +255,24 @@ class SnapManager(SoftwareManager): self.categories_downloader.join() suggestions, threads = [], [] + installed = {i.name.lower() for i in self.read_installed(disk_loader=None).installed} if filter_installed else None for l in file.text.split('\n'): if l: if limit <= 0 or len(suggestions) < limit: sug = l.strip().split('=') - cached_sug = self.suggestions_cache.get(sug[1]) + name = sug[1] - if cached_sug: - res.append(cached_sug) - else: - t = Thread(target=self._fill_suggestion, args=(sug[1], SuggestionPriority(int(sug[0])), res)) - t.start() - threads.append(t) - time.sleep(0.001) # to avoid being blocked + if not installed or name not in installed: + cached_sug = self.suggestions_cache.get(name) + + if cached_sug: + res.append(cached_sug) + else: + t = Thread(target=self._fill_suggestion, args=(name, SuggestionPriority(int(sug[0])), res)) + t.start() + threads.append(t) + time.sleep(0.001) # to avoid being blocked else: break diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index d54c01e8..679f6f66 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -782,6 +782,17 @@ class WebApplicationManager(SoftwareManager): if suggestions: suggestion_list = list(suggestions.values()) suggestion_list.sort(key=lambda s: s.get('priority', 0), reverse=True) + + if filter_installed: + installed = self.read_installed(disk_loader=None) + url_map = {self._strip_url_protocol(s['url']): idx for idx, s in enumerate(suggestion_list)} + + if installed.installed: + for i in installed.installed: + s_url = self._strip_url_protocol(i.url) + if s_url in url_map: + suggestion_list.pop(url_map[s_url]) + to_map = suggestion_list if limit <= 0 else suggestion_list[0:limit] res = [self._map_suggestion(s) for s in to_map] diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index 92011cd0..5f28f914 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -328,10 +328,10 @@ class GenericSoftwareManager(SoftwareManager): return warnings - def _fill_suggestions(self, suggestions: list, man: SoftwareManager, limit: int): + def _fill_suggestions(self, suggestions: list, man: SoftwareManager, limit: int, filter_installed: bool): if self._can_work(man): mti = time.time() - man_sugs = man.list_suggestions(limit) + man_sugs = man.list_suggestions(limit=limit, filter_installed=filter_installed) mtf = time.time() self.logger.info(man.__class__.__name__ + ' took {0:.2f} seconds'.format(mtf - mti)) @@ -346,7 +346,7 @@ class GenericSoftwareManager(SoftwareManager): if self.managers and internet.is_available(self.context.http_client, self.context.logger): suggestions, threads = [], [] for man in self.managers: - t = Thread(target=self._fill_suggestions, args=(suggestions, man, int(self.config['suggestions']['by_type']))) + t = Thread(target=self._fill_suggestions, args=(suggestions, man, int(self.config['suggestions']['by_type']), filter_installed)) t.start() threads.append(t) diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index eacc72dc..a3edd855 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -393,9 +393,10 @@ class FindSuggestions(AsyncAction): def __init__(self, man: SoftwareManager): super(FindSuggestions, self).__init__() self.man = man + self.filter_installed = False def run(self): - sugs = self.man.list_suggestions(limit=-1) + sugs = self.man.list_suggestions(limit=-1, filter_installed=self.filter_installed) self.notify_finished({'pkgs_found': [s.package for s in sugs] if sugs is not None else [], 'error': None}) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 442470c3..1d08c776 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -189,6 +189,17 @@ class ManageWindow(QWidget): self.bt_installed.setStyleSheet(toolbar_button_style('#A94E0A')) self.ref_bt_installed = self.toolbar.addWidget(self.bt_installed) + if config['suggestions']['enabled']: + self.bt_suggestions = QPushButton() + self.bt_suggestions.setText('Suggestions') + self.bt_suggestions.setIcon(QIcon(resource.get_path('img/suggestions.svg'))) + self.bt_suggestions.setStyleSheet(toolbar_button_style('#FF8000')) + self.bt_suggestions.clicked.connect(self.read_suggestions) + self.ref_bt_suggestions = self.toolbar.addWidget(self.bt_suggestions) + else: + self.bt_suggestions = None + self.ref_bt_suggestions = None + self.bt_refresh = QPushButton() self.bt_refresh.setToolTip(i18n['manage_window.bt.refresh.tooltip']) self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg'))) @@ -492,6 +503,15 @@ class ManageWindow(QWidget): self.thread_refresh.pkg_types = pkg_types self.thread_refresh.start() + def read_suggestions(self): + self.input_search.clear() + self._handle_console_option(False) + self.ref_checkbox_updates.setVisible(False) + self.ref_checkbox_only_apps.setVisible(False) + self._begin_action('Retrieving suggestions', keep_bt_installed=False, clear_filters=not self.recent_uninstall) + self.thread_suggestions.filter_installed = True + self.thread_suggestions.start() + def _finish_refresh_apps(self, res: dict, as_installed: bool = True): self.finish_action() self.ref_checkbox_only_apps.setVisible(bool(res['installed'])) @@ -671,6 +691,7 @@ class ManageWindow(QWidget): if pkgs_info['apps_count'] == 0: if self.first_refresh or self.types_changed: self._begin_search('') + self.thread_suggestions.filter_installed = False self.thread_suggestions.start() return else: @@ -870,6 +891,10 @@ class ManageWindow(QWidget): self.label_status.setText(action_label + "...") self.ref_bt_upgrade.setVisible(False) self.ref_bt_refresh.setVisible(False) + + if self.ref_bt_suggestions: + self.ref_bt_suggestions.setVisible(False) + self.checkbox_only_apps.setEnabled(False) self.table_apps.setEnabled(False) self.checkbox_updates.setEnabled(False) @@ -913,6 +938,9 @@ class ManageWindow(QWidget): self.checkbox_updates.setEnabled(True) self.progress_controll_enabled = True + if self.ref_bt_suggestions: + self.ref_bt_suggestions.setVisible(True) + if self.pkgs: self.ref_input_name_filter.setVisible(True) self.update_bt_upgrade() @@ -1113,10 +1141,6 @@ class ManageWindow(QWidget): def _show_settings_menu(self): menu_row = QMenu() - # TODO - # action_suggestions = QAction(self.i18n['manage_window.settings.suggestions']) - # action_suggestions.setIcon(QIcon()) - if isinstance(self.manager, GenericSoftwareManager): action_gems = QAction(self.i18n['manage_window.settings.gems']) action_gems.setIcon(self.icon_app) diff --git a/bauh/view/resources/img/suggestions.svg b/bauh/view/resources/img/suggestions.svg new file mode 100644 index 00000000..bbcfdb99 --- /dev/null +++ b/bauh/view/resources/img/suggestions.svg @@ -0,0 +1,168 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file