theitd icon

recoll_albert

theitd | PRO | 08/08/22 01:24:16 PM UTC (Edited) | 0 ⭐ | 397 👁️ | Never ⏰ | []
Python |

8.06 KB

|

None

|

0 👍

/

0 👎

"""Recoll."""
import os
import inspect
from sys import platform
import traceback
from pathlib import Path
from collections import Counter
import mimetypes
 
# TODO: Check if this fails and return a more comprehensive error message for the user
from recoll import recoll
 
from albert import *
 
__iid__ = "PythonInterface/v0.2"
__prettyname__ = "Recoll"
__title__ = "Recoll for Albert"
__version__ = "0.1.1"
#__trigger__ = "rc " # Use this if you want it to be only triggered by rc <query>
__authors__ = "Gerard Simons"
__dependencies__ = []
__homepage__ = "https://github.com/gerardsimons/recoll-albert/blob/master/recoll/recoll"
 
HOME_DIR = os.environ["HOME"]
 
icon_path = Path.home()  / "recoll"
cache_path = Path.home()   / "recoll"
config_path = Path.home() / "recoll"
data_path = Path.home() / "recoll"
dev_mode = True
 
# If set to to true it removes duplicate documents hits that share the same URL, such as epubs or other archives.
# Note that this means the number of results returned may be different from the actual recoll results
remove_duplicates = True
 
# String definitions
OPEN_WITH_DEFAULT_APP = "Open with default application"
REVEAL_IN_FILE_BROWSER = "Reveal in file browser"
OPEN_TERMINAL_AT_THIS_PATH = "Open terminal at this path"
COPY_FILE_CLIPBOARD = "Copy file to clipboard"
COPY_PATH_CLIPBOARD = "Copy path to clipboard"
 
# plugin main functions -----------------------------------------------------------------------
 
def initialize():
    """Called when the extension is loaded (ticked in the settings) - blocking."""
 
    # create plugin locations
    for p in (cache_path, config_path, data_path):
        p.mkdir(parents=False, exist_ok=True)
 
def query_recoll(query_str, max_results=10, max_chars=80, context_words=4, verbose=False):
    """
    Query recoll index for simple query string and return the filenames in order of relevancy
    :param query_str:
    :param max_results:
    :return:
    """
    if not query_str:
        return []
 
    db = recoll.connect()
    db.setAbstractParams(maxchars=max_chars, contextwords=context_words)
    query = db.query()
    nres = query.execute(query_str)
    docs = []
    if nres > max_results:
        nres = max_results
    for i in range(nres):
        doc = query.fetchone() # TODO: JUst use fetchall and return the lot?!
        docs.append(doc)
 
    return docs
 
 
def path_from_url(url: str) -> str:
    if not url.startswith('file://'):
        return None
    else:
        return url.replace("file://", "")
 
 
def get_open_dir_action(dir: str):
    if platform == "linux" or platform == "linux2":
        return ProcAction(text=REVEAL_IN_FILE_BROWSER, commandline=["xdg-open", dir])
    elif platform == "darwin":
        return ProcAction(text=REVEAL_IN_FILE_BROWSER, commandline=["open", dir])
    elif platform == "win32": # From https://stackoverflow.com/a/2878744/916382
        return FuncAction(text=REVEAL_IN_FILE_BROWSER, callable=lambda : os.startfile(dir))
 
 
def doc_to_icon_path(doc) -> str:
    """ Attempts to convert a mime type to a text string that is accepted by """
    mime_str = getattr(doc, "mtype", None)
    if not mime_str:
        return albert.iconLookup("unknown")
    mime_str = mime_str.replace("/", "-")
    icon_path = iconLookup(mime_str)
    if not icon_path:
        icon_path = iconLookup("unknown")
    return icon_path
 
 
def remove_duplicate_docs(docs: list):
    """
    Removes Recoll docs that have the same URL but actually refer to different files, for example an epub file
    which contains HTML files will have multiple docs for each but they all refer to the same epub file.
    :param docs: the original list of documents
    :return: the same docs param but with the docs removed that share the same URL attribute
    """
    urls = [x.url for x in docs]
    url_count = Counter(urls)
 
    duplicates = [k for k in url_count.keys() if url_count[k] > 1]
    # Merge duplicate results, this might happen becase it actually consists of more than 1 file, like an epub
    # We adopt the relevancy rating of the max one
    for dup in duplicates:
        # Just take the one with the highest relevancy
        best_doc = None
        best_rating = -1
        for doc in [x for x in docs if x.url == dup]:
            rating = float(doc.relevancyrating.replace("%", ""))
            if rating > best_rating:
                best_doc = doc
                best_rating = rating
 
        docs = [x for x in docs if x.url != dup]
        docs.append(best_doc)
    return docs
 
def recoll_docs_as_items(docs: list):
    """Return an item - ready to be appended to the items list and be rendered by Albert."""
 
    items = []
 
    # First we find duplicates if so configured
    if remove_duplicates:
        docs = remove_duplicate_docs(docs)
 
    for doc in docs:
        path = path_from_url(doc.url) # The path is not always given as an attribute by recoll doc
        dir = os.path.dirname(path)
        file_extension = Path(path).suffix # Get the file extension and work out the mimetype
        mime_type = mimetypes.types_map[file_extension]
 
        dir_open = get_open_dir_action(dir)
 
        if path:
            actions=[
                    UrlAction(
                        OPEN_WITH_DEFAULT_APP, 
                        doc.url),
                    ClipAction(text=COPY_PATH_CLIPBOARD,
                               clipboardText=path),
                    TermAction(text=COPY_FILE_CLIPBOARD,
                                script="xclip -selection clipboard -t " + mime_type + " -i '" + path + "'",
                                behavior=TermAction.CloseBehavior(0),
                                cwd="/usr/bin")
                ]
 
            if dir_open:
                actions.append(dir_open)
 
            # Add the item
            items.append(Item(
                id=__prettyname__,
                icon=doc_to_icon_path(doc),
                text=doc.filename,
                subtext=dir,
                completion="",
                actions=actions
            ))
    return items
 
 
def handleQuery(query) -> list:
    """Hook that is called by albert with *every new keypress*."""  # noqa
    results = []
 
    try:
        if __trigger__  and not query.isTriggered:
            return []
        # be backwards compatible with v0.2
        if "disableSort" in dir(query):
            query.disableSort()
 
        results_setup = setup(query)
        if results_setup:
            return results_setup
 
        docs = query_recoll(query.string)
        results = recoll_docs_as_items(docs)
    except Exception:  # user to report error
        if dev_mode:  # let exceptions fly!
            print(traceback.format_exc())
            raise
 
        results.insert(
            0,
            albert.Item(
                id=__prettyname__,
                icon=icon_path,
                text="Something went wrong! Press [ENTER] to copy error and report it",
                actions=[
                    albert.ClipAction(
                        f"Copy error - report it to {__homepage__[8:]}",
                        f"{traceback.format_exc()}",
                    )
                ],
            ),
        )
 
    return results
 
 
# supplementary functions ---------------------------------------------------------------------
def save_data(data: str, data_name: str):
    """Save a piece of data in the configuration directory."""
    with open(config_path / data_name, "w") as f:
        f.write(data)
 
 
def load_data(data_name) -> str:
    """Load a piece of data from the configuration directory."""
    with open(config_path / data_name, "r") as f:
        data = f.readline().strip().split()[0]
 
    return data
 
 
def setup(query):
    """Setup is successful if an empty list is returned.
 
    Use this function if you need the user to provide you data
    """
 
    results = []
    return results
 
# In case the __trigger__ was not set at all we set it to the empty string
try:
    __trigger__
except NameError:
    __trigger__ = ""
 

Comments