Advertisement
theitd

recoll_albert

Jul 20th, 2022 (edited)
75
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.06 KB | None | 0 0
  1. """Recoll."""
  2. import os
  3. import inspect
  4. from sys import platform
  5. import traceback
  6. from pathlib import Path
  7. from collections import Counter
  8. import mimetypes
  9.  
  10. # TODO: Check if this fails and return a more comprehensive error message for the user
  11. from recoll import recoll
  12.  
  13. from albert import *
  14.  
  15. __iid__ = "PythonInterface/v0.2"
  16. __prettyname__ = "Recoll"
  17. __title__ = "Recoll for Albert"
  18. __version__ = "0.1.1"
  19. #__trigger__ = "rc " # Use this if you want it to be only triggered by rc <query>
  20. __authors__ = "Gerard Simons"
  21. __dependencies__ = []
  22. __homepage__ = "https://github.com/gerardsimons/recoll-albert/blob/master/recoll/recoll"
  23.  
  24. HOME_DIR = os.environ["HOME"]
  25.  
  26. icon_path = Path.home()  / "recoll"
  27. cache_path = Path.home()   / "recoll"
  28. config_path = Path.home() / "recoll"
  29. data_path = Path.home() / "recoll"
  30. dev_mode = True
  31.  
  32. # If set to to true it removes duplicate documents hits that share the same URL, such as epubs or other archives.
  33. # Note that this means the number of results returned may be different from the actual recoll results
  34. remove_duplicates = True
  35.  
  36. # String definitions
  37. OPEN_WITH_DEFAULT_APP = "Open with default application"
  38. REVEAL_IN_FILE_BROWSER = "Reveal in file browser"
  39. OPEN_TERMINAL_AT_THIS_PATH = "Open terminal at this path"
  40. COPY_FILE_CLIPBOARD = "Copy file to clipboard"
  41. COPY_PATH_CLIPBOARD = "Copy path to clipboard"
  42.  
  43. # plugin main functions -----------------------------------------------------------------------
  44.  
  45. def initialize():
  46.     """Called when the extension is loaded (ticked in the settings) - blocking."""
  47.  
  48.     # create plugin locations
  49.     for p in (cache_path, config_path, data_path):
  50.         p.mkdir(parents=False, exist_ok=True)
  51.  
  52. def query_recoll(query_str, max_results=10, max_chars=80, context_words=4, verbose=False):
  53.     """
  54.    Query recoll index for simple query string and return the filenames in order of relevancy
  55.    :param query_str:
  56.    :param max_results:
  57.    :return:
  58.    """
  59.     if not query_str:
  60.         return []
  61.  
  62.     db = recoll.connect()
  63.     db.setAbstractParams(maxchars=max_chars, contextwords=context_words)
  64.     query = db.query()
  65.     nres = query.execute(query_str)
  66.     docs = []
  67.     if nres > max_results:
  68.         nres = max_results
  69.     for i in range(nres):
  70.         doc = query.fetchone() # TODO: JUst use fetchall and return the lot?!
  71.         docs.append(doc)
  72.  
  73.     return docs
  74.  
  75.  
  76. def path_from_url(url: str) -> str:
  77.     if not url.startswith('file://'):
  78.         return None
  79.     else:
  80.         return url.replace("file://", "")
  81.  
  82.  
  83. def get_open_dir_action(dir: str):
  84.     if platform == "linux" or platform == "linux2":
  85.         return ProcAction(text=REVEAL_IN_FILE_BROWSER, commandline=["xdg-open", dir])
  86.     elif platform == "darwin":
  87.         return ProcAction(text=REVEAL_IN_FILE_BROWSER, commandline=["open", dir])
  88.     elif platform == "win32": # From https://stackoverflow.com/a/2878744/916382
  89.         return FuncAction(text=REVEAL_IN_FILE_BROWSER, callable=lambda : os.startfile(dir))
  90.  
  91.  
  92. def doc_to_icon_path(doc) -> str:
  93.     """ Attempts to convert a mime type to a text string that is accepted by """
  94.     mime_str = getattr(doc, "mtype", None)
  95.     if not mime_str:
  96.         return albert.iconLookup("unknown")
  97.     mime_str = mime_str.replace("/", "-")
  98.     icon_path = iconLookup(mime_str)
  99.     if not icon_path:
  100.         icon_path = iconLookup("unknown")
  101.     return icon_path
  102.  
  103.  
  104. def remove_duplicate_docs(docs: list):
  105.     """
  106.    Removes Recoll docs that have the same URL but actually refer to different files, for example an epub file
  107.    which contains HTML files will have multiple docs for each but they all refer to the same epub file.
  108.    :param docs: the original list of documents
  109.    :return: the same docs param but with the docs removed that share the same URL attribute
  110.    """
  111.     urls = [x.url for x in docs]
  112.     url_count = Counter(urls)
  113.  
  114.     duplicates = [k for k in url_count.keys() if url_count[k] > 1]
  115.     # Merge duplicate results, this might happen becase it actually consists of more than 1 file, like an epub
  116.     # We adopt the relevancy rating of the max one
  117.     for dup in duplicates:
  118.         # Just take the one with the highest relevancy
  119.         best_doc = None
  120.         best_rating = -1
  121.         for doc in [x for x in docs if x.url == dup]:
  122.             rating = float(doc.relevancyrating.replace("%", ""))
  123.             if rating > best_rating:
  124.                 best_doc = doc
  125.                 best_rating = rating
  126.  
  127.         docs = [x for x in docs if x.url != dup]
  128.         docs.append(best_doc)
  129.     return docs
  130.  
  131. def recoll_docs_as_items(docs: list):
  132.     """Return an item - ready to be appended to the items list and be rendered by Albert."""
  133.  
  134.     items = []
  135.  
  136.     # First we find duplicates if so configured
  137.     if remove_duplicates:
  138.         docs = remove_duplicate_docs(docs)
  139.  
  140.     for doc in docs:
  141.         path = path_from_url(doc.url) # The path is not always given as an attribute by recoll doc
  142.         dir = os.path.dirname(path)
  143.         file_extension = Path(path).suffix # Get the file extension and work out the mimetype
  144.         mime_type = mimetypes.types_map[file_extension]
  145.  
  146.         dir_open = get_open_dir_action(dir)
  147.  
  148.         if path:
  149.             actions=[
  150.                     UrlAction(
  151.                         OPEN_WITH_DEFAULT_APP,
  152.                         doc.url),
  153.                     ClipAction(text=COPY_PATH_CLIPBOARD,
  154.                                clipboardText=path),
  155.                     TermAction(text=COPY_FILE_CLIPBOARD,
  156.                                 script="xclip -selection clipboard -t " + mime_type + " -i '" + path + "'",
  157.                                 behavior=TermAction.CloseBehavior(0),
  158.                                 cwd="/usr/bin")
  159.                 ]
  160.  
  161.             if dir_open:
  162.                 actions.append(dir_open)
  163.  
  164.             # Add the item
  165.             items.append(Item(
  166.                 id=__prettyname__,
  167.                 icon=doc_to_icon_path(doc),
  168.                 text=doc.filename,
  169.                 subtext=dir,
  170.                 completion="",
  171.                 actions=actions
  172.             ))
  173.     return items
  174.  
  175.  
  176. def handleQuery(query) -> list:
  177.     """Hook that is called by albert with *every new keypress*."""  # noqa
  178.     results = []
  179.  
  180.     try:
  181.         if __trigger__  and not query.isTriggered:
  182.             return []
  183.         # be backwards compatible with v0.2
  184.         if "disableSort" in dir(query):
  185.             query.disableSort()
  186.  
  187.         results_setup = setup(query)
  188.         if results_setup:
  189.             return results_setup
  190.  
  191.         docs = query_recoll(query.string)
  192.         results = recoll_docs_as_items(docs)
  193.     except Exception:  # user to report error
  194.         if dev_mode:  # let exceptions fly!
  195.             print(traceback.format_exc())
  196.             raise
  197.  
  198.         results.insert(
  199.             0,
  200.             albert.Item(
  201.                 id=__prettyname__,
  202.                 icon=icon_path,
  203.                 text="Something went wrong! Press [ENTER] to copy error and report it",
  204.                 actions=[
  205.                     albert.ClipAction(
  206.                         f"Copy error - report it to {__homepage__[8:]}",
  207.                         f"{traceback.format_exc()}",
  208.                     )
  209.                 ],
  210.             ),
  211.         )
  212.  
  213.     return results
  214.  
  215.  
  216. # supplementary functions ---------------------------------------------------------------------
  217. def save_data(data: str, data_name: str):
  218.     """Save a piece of data in the configuration directory."""
  219.     with open(config_path / data_name, "w") as f:
  220.         f.write(data)
  221.  
  222.  
  223. def load_data(data_name) -> str:
  224.     """Load a piece of data from the configuration directory."""
  225.     with open(config_path / data_name, "r") as f:
  226.         data = f.readline().strip().split()[0]
  227.  
  228.     return data
  229.  
  230.  
  231. def setup(query):
  232.     """Setup is successful if an empty list is returned.
  233.  
  234.    Use this function if you need the user to provide you data
  235.    """
  236.  
  237.     results = []
  238.     return results
  239.  
  240. # In case the __trigger__ was not set at all we set it to the empty string
  241. try:
  242.     __trigger__
  243. except NameError:
  244.     __trigger__ = ""
  245.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement