From a933c5a8b269f94278594a9834d1a4bef24c1bbc Mon Sep 17 00:00:00 2001 From: KR <79979949+justfoolingaround@users.noreply.github.com> Date: Tue, 14 Jun 2022 00:24:12 +0545 Subject: [PATCH 1/6] Feat: fzf --- hart-cli.py | 366 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 256 insertions(+), 110 deletions(-) diff --git a/hart-cli.py b/hart-cli.py index 4f04b04..0547856 100755 --- a/hart-cli.py +++ b/hart-cli.py @@ -1,132 +1,278 @@ -#!/usr/bin/env python3 +import enum +import pathlib +import re +import shutil +import subprocess +import sys +import tempfile +import time +from collections import deque import httpx -from bs4 import BeautifulSoup as bs -import pyperclip as clip -import os -import subprocess -from os.path import expanduser +try: + import pyperclip +except ImportError: + pyperclip = None -headers = { - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0" -} +has_kitty = bool(shutil.which("kitty")) -client = httpx.Client(headers=headers, follow_redirects=True) +RSS_FEED_API = "https://yande.re/post/piclens" -home = expanduser("~") -download_path = f"{home}/pix/hart-cli" -os.system(f"mkdir -p {download_path}") -item = 0 -page_num = 1 +SEARCHER_REGEX = re.compile( + r"(?s)" + r"\s+" + r"(?P<title>.+?)\s+" + r"(?P.+?/(?P\d+)).+?" + r'\s+' + r'', +) -url = f"https://yande.re/post?page={page_num}" -page = client.get(url) -links_arr_full = [] -links_arr_preview = [] +fzf_executable = "fzf" +fzf_args = [ + fzf_executable, + "--color=fg:#d60a79", + "--reverse", + "--height=50%", + "--cycle", + "--no-mouse", +] -def get_new_urls(): - global url - global page - global page_num - global soup - global main_content - global links_full - global links_arr_full - global links_preview - global links_arr_preview - - os.system("clear") - links_arr_full.clear - links_arr_preview.clear - - soup = bs(page.content, "html.parser") - main_content = soup.find(id="post-list-posts") - main_content = str(main_content) - main_content = main_content.replace("smallimg", "largeimg") - main_content = bs(main_content, features="lxml") - main_content = main_content.find(id="post-list-posts") +def sanitize_filename(f): + return "".join("_" if _ in '<>:"/\\|?*' else _ for _ in f).strip() - links_full = main_content.find_all_next("a", class_="directlink largeimg") - links_arr_full = [] - links_preview = main_content.find_all_next("img", class_="preview") - links_arr_preview = [] - for link in links_full: - link_url = link["href"] - links_arr_full.append(link_url) - for link in links_preview: - link_url = link["src"] - links_arr_preview.append(link_url) -def next(): - global item - global page_num - - os.system("clear") - if item != len(links_arr_preview)-1: - item += 1 +class UserMenuSelection(enum.Enum): + KEEP_BROWSING = "b" + QUIT = "q" + + +class UserBrowseSelection(enum.Enum): + + COPY_TO_CLIPBOARD = "c" + DOWNLOAD = "d" + PERSIST_SELECTION = "p" + PREVIEW = "r" + + +def iter_results(session, tags=None): + + params = {} + + if tags: + params["tags"] = tags + + content = {} + page = 0 + + while content is not None: + content = None + params.update({"page": page + 1}) + + for content in SEARCHER_REGEX.finditer( + session.get(RSS_FEED_API, params=params).text + ): + yield content.groupdict() + + page += 1 + time.sleep(1.0) + + +global_deque = deque() + + +def prompt_via_fzf(genexp, *, global_dequeue: deque = global_deque, is_last=False): + + fzf_process = subprocess.Popen( + fzf_args + ["--multi"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + component_holder = {} + previous_session = b"" + + for result in global_deque: + + console_out = ("{title} / {id}".format_map(result) + "\n").encode() + component_holder[console_out] = result + previous_session += console_out + + if global_deque: + fzf_process.stdin.write(previous_session) + fzf_process.stdin.flush() + + while fzf_process.returncode is None and not is_last: + + try: + result = next(genexp) + except StopIteration: + is_last = True + break + + console_out = ("{title} / {id}".format_map(result) + "\n").encode() + + if console_out in component_holder: + continue + + component_holder.update({console_out: result}) + global_dequeue.append(result) + + try: + fzf_process.stdin.write(console_out) + fzf_process.stdin.flush() + except OSError: + break + + fzf_process.wait() + + return [component_holder[_] for _ in fzf_process.stdout.readlines()], is_last + + +def user_fzf_choice(args): + + fzf_process = subprocess.Popen( + fzf_args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + fzf_process.stdin.write(b"\n".join((_.encode() for _ in args)) + b"\n") + fzf_process.stdin.close() + fzf_process.wait() + + return fzf_process.stdout.read()[:-1].decode() + + +def copy_opt(session, user_prompt): + return pyperclip.copy(user_prompt["image_url"]) + + +def download_opt(session, user_prompt): + download_path = pathlib.Path(sanitize_filename(user_prompt["title"]) + ".jpg") + + with open(download_path, "wb") as image_file: + image_file.write(session.get(user_prompt["image_url"]).content) + + print(f"hart: downloaded to {download_path.as_posix()!r}") + + +def preview_opt(session, user_prompt): + + temp_file_path = pathlib.Path(tempfile.gettempdir()) / "hart-preview.jpg" + + with open(temp_file_path, "wb") as image_file: + image_file.write(session.get(user_prompt["preview"]).content) + + if has_kitty: + process = subprocess.call( + [ + "kitty", + "--hold", + "--window-size=100%", + "--window-position=0%", + temp_file_path.as_posix(), + ] + ) else: - page_num += 1 - item = 1 - get_new_urls() -def previous(): - global item - global page_num - global links_arr_preview + if sys.platform == "darwin": + opener = "open" + else: + if sys.platform == "win32": + opener = "start" + else: + opener = "xdg-open" + process = subprocess.call([opener, temp_file_path.as_posix()], shell=True) + + if process != 0: + return print(f"hart: failed to open preview, opener threw error {process}") + + +def browse_options(session, user_prompt, *, persist=False, persist_with=None): + + print("hart: {title!r} / yandere[{id}]".format_map(user_prompt)) + + if persist_with is None: + + options = { + "[d]ownload": UserBrowseSelection.DOWNLOAD, + "p[r]eview": UserBrowseSelection.PREVIEW, + } + + if not persist: + options[ + "[p]ersist selection for next in queue" + ] = UserBrowseSelection.PERSIST_SELECTION + + if pyperclip is not None: + options["[c]opy to clipboard"] = UserBrowseSelection.COPY_TO_CLIPBOARD + + user_choice = options.get(user_fzf_choice(options)) + + if persist and persist_with is None: + persist_with = user_choice - os.system("clear") - if item != 1: - item -= 1 else: - page_num -= 1 - get_new_urls() - item = len(links_arr_preview)-1 + user_choice = persist_with -def download(): - global item - global links_arr_full - global download_path + if user_choice is None: + return - command = 'echo ' + links_arr_full[item] + ' | cut -d "%" -f 2 |cut -b 3-8' - name = subprocess.check_output(command, shell=True, text=True, encoding='utf_8') - name = name.strip('\n') - name = str(name)+".jpg" - command = "curl -s -o " + download_path + "/" + name + " " + links_arr_full[item] - os.system(command) - os.system("clear") + if user_choice == UserBrowseSelection.PERSIST_SELECTION: + return browse_options(session, user_prompt, persist=True) -get_new_urls() + if user_choice == UserBrowseSelection.COPY_TO_CLIPBOARD: + copy_opt(session, user_prompt) -while True: - command = "curl -s -o /tmp/hart-preview.jpg " + links_arr_preview[item] - os.system(command) - command = "convert /tmp/hart-preview.jpg -resize 500x500 /tmp/hart-preview.jpg" - os.system(command) - command = "kitty +icat --place 100x100@0x0 /tmp/hart-preview.jpg" - os.system(command) - print("next:\t\tn") - print("previous:\tp") - print("download:\td") - print("copy URL:\tc") - print("quit:\t\tq") - choice= input() - if choice == "n": - next() - elif choice == "p": - previous() - elif choice == "d": - download() - elif choice == "c": - clip.copy(links_arr_full[item]) - os.system('clear') - elif choice == "q": - os.system('clear') - exit() - else: - print("invaled awnser") - exit(0) + if user_choice == UserBrowseSelection.DOWNLOAD: + download_opt(session, user_prompt) + if user_choice == UserBrowseSelection.PREVIEW: + preview_opt(session, user_prompt) + return browse_options( + session, user_prompt, persist=persist, persist_with=persist_with + ) + + if persist: + return persist_with + + +def __main__(query=None): + + http_client = httpx.Client(headers={"User-Agent": "uwu"}) + + genexp = iter_results(http_client, query) + + user_choice = None + is_last = False + + options = { + "[b]rowse harts": UserMenuSelection.KEEP_BROWSING, + "[q]uit": UserMenuSelection.QUIT, + } + + while user_choice != UserMenuSelection.QUIT: + user_choice = options.get(user_fzf_choice(options), UserMenuSelection.QUIT) + + if user_choice == UserMenuSelection.KEEP_BROWSING: + + selection, is_last = prompt_via_fzf(genexp, is_last=is_last) + + if selection: + + persist_with = None + + for _ in selection: + persist_with = browse_options( + http_client, + _, + persist=persist_with is not None, + persist_with=persist_with, + ) + +if __name__ == "__main__": + __main__(*sys.argv[1:]) From 066311266f854d8f87c028ff559133aee7fb5960 Mon Sep 17 00:00:00 2001 From: KR <79979949+justfoolingaround@users.noreply.github.com> Date: Tue, 14 Jun 2022 00:38:10 +0545 Subject: [PATCH 2/6] Chore: Kitty args --- hart-cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hart-cli.py b/hart-cli.py index 0547856..9bf0a0a 100755 --- a/hart-cli.py +++ b/hart-cli.py @@ -171,9 +171,9 @@ def preview_opt(session, user_prompt): process = subprocess.call( [ "kitty", - "--hold", - "--window-size=100%", - "--window-position=0%", + "+icat", + "--place", + "100x100@0x0", temp_file_path.as_posix(), ] ) From 0c650118217631b64cb594ba024d491198cb3d3d Mon Sep 17 00:00:00 2001 From: KR <79979949+justfoolingaround@users.noreply.github.com> Date: Tue, 14 Jun 2022 00:39:20 +0545 Subject: [PATCH 3/6] Chore: Add shebang back --- hart-cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hart-cli.py b/hart-cli.py index 9bf0a0a..5d1dc41 100755 --- a/hart-cli.py +++ b/hart-cli.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + import enum import pathlib import re From f4200533da946c8cd4913c2e2fc39c67b2e08c5d Mon Sep 17 00:00:00 2001 From: KR <79979949+justfoolingaround@users.noreply.github.com> Date: Tue, 14 Jun 2022 00:54:13 +0545 Subject: [PATCH 4/6] Remove: `preview` for persist mode --- hart-cli.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/hart-cli.py b/hart-cli.py index 5d1dc41..59383e1 100755 --- a/hart-cli.py +++ b/hart-cli.py @@ -202,13 +202,15 @@ def browse_options(session, user_prompt, *, persist=False, persist_with=None): options = { "[d]ownload": UserBrowseSelection.DOWNLOAD, - "p[r]eview": UserBrowseSelection.PREVIEW, } if not persist: - options[ - "[p]ersist selection for next in queue" - ] = UserBrowseSelection.PERSIST_SELECTION + options.update( + { + "[p]ersist selection for next in queue": UserBrowseSelection.PERSIST_SELECTION, + "p[r]eview": UserBrowseSelection.PREVIEW, + } + ) if pyperclip is not None: options["[c]opy to clipboard"] = UserBrowseSelection.COPY_TO_CLIPBOARD @@ -276,5 +278,6 @@ def __main__(query=None): persist_with=persist_with, ) + if __name__ == "__main__": __main__(*sys.argv[1:]) From 923c79761c938751f45226aaa83edd53ac993b90 Mon Sep 17 00:00:00 2001 From: KR <79979949+justfoolingaround@users.noreply.github.com> Date: Tue, 14 Jun 2022 01:09:59 +0545 Subject: [PATCH 5/6] Remove: Recursive calls from `preview` --- hart-cli.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/hart-cli.py b/hart-cli.py index 59383e1..6f61ba3 100755 --- a/hart-cli.py +++ b/hart-cli.py @@ -237,9 +237,6 @@ def browse_options(session, user_prompt, *, persist=False, persist_with=None): if user_choice == UserBrowseSelection.PREVIEW: preview_opt(session, user_prompt) - return browse_options( - session, user_prompt, persist=persist, persist_with=persist_with - ) if persist: return persist_with From feaa889e8ddec0a523eebfb7cfe523b1b7891ac7 Mon Sep 17 00:00:00 2001 From: KR <79979949+justfoolingaround@users.noreply.github.com> Date: Tue, 14 Jun 2022 01:11:37 +0545 Subject: [PATCH 6/6] Chore: Undo last commit --- hart-cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hart-cli.py b/hart-cli.py index 6f61ba3..2bc1c8b 100755 --- a/hart-cli.py +++ b/hart-cli.py @@ -237,7 +237,10 @@ def browse_options(session, user_prompt, *, persist=False, persist_with=None): if user_choice == UserBrowseSelection.PREVIEW: preview_opt(session, user_prompt) - + return browse_options( + session, user_prompt, persist=persist, persist_with=persist_with + ) + if persist: return persist_with