270 lines
9.0 KiB
Python
Executable File
270 lines
9.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# flake8: noqa: E501
|
|
"""
|
|
Bagheera Search Tool - CLI Client
|
|
"""
|
|
|
|
__appname__ = "BagheeraSearch"
|
|
__version__ = "1.0"
|
|
__author__ = "Ignacio Serantes"
|
|
__email__ = "kde@aynoa.net"
|
|
__license__ = "LGPL"
|
|
__status__ = "Production"
|
|
# "Prototype, Development, Alpha, Beta, Production, Stable, Deprecated"
|
|
|
|
import argparse
|
|
import json
|
|
import signal
|
|
import sys
|
|
from pathlib import Path
|
|
# from baloo_tools import get_resolution
|
|
# from date_query_parser import parse_date
|
|
from bagheera_search_lib import BagheeraSearcher
|
|
|
|
# --- CONFIGURATION ---
|
|
PROG_NAME = "Bagheera Search Tool"
|
|
PROG_ID = "bagheerasearch"
|
|
PROG_VERSION = "1.0"
|
|
PROG_BY = "Ignacio Serantes"
|
|
PROG_DATE = "2026-03-19"
|
|
|
|
CONFIG_DIR = Path.home() / ".config" / PROG_ID
|
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
|
|
|
|
def load_config() -> dict:
|
|
"""Loads user configuration from disk."""
|
|
if CONFIG_FILE.exists():
|
|
try:
|
|
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
print(f"Warning: Could not load config file: {e}")
|
|
return {}
|
|
|
|
|
|
def save_config(config: dict) -> None:
|
|
"""Saves user configuration to disk."""
|
|
try:
|
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
json.dump(config, f, indent=4)
|
|
except OSError as e:
|
|
print(f"Warning: Could not save config file: {e}")
|
|
|
|
|
|
def print_help_query() -> None:
|
|
"""Prints the detailed help for query syntax."""
|
|
help_query = f"""Help updated to 2025-01-01.
|
|
|
|
Baloo offers a rich syntax for searching through your files. Certain attributes of a file can be searched through.
|
|
|
|
For example 'type' can be used to filter for files based on their general type:
|
|
|
|
type:Audio or type:Document
|
|
|
|
The following comparison operators are supported, but note that 'not equal' operator is not available.
|
|
· : - contains (only for text comparison)
|
|
· = - equal
|
|
· > - greater than
|
|
· >= - greater than or equal to
|
|
· < - less than
|
|
· <= - less than or equal to
|
|
|
|
Currently the following types are supported:
|
|
|
|
· Archive
|
|
· Folder
|
|
· Audio
|
|
· Video
|
|
· Image
|
|
· Document
|
|
· Spreadsheet
|
|
· Presentation
|
|
· Text
|
|
|
|
These expressions can be combined using AND or OR and additional parenthesis, but note that 'NOT' logical operator is not available.
|
|
|
|
[... omitted for brevity, but includes the full list of searchable properties as in your original script ...]
|
|
|
|
{PROG_NAME} recognizes some natural language sentences in English, as long as they are capitalized, and transforms them into queries that can be interpreted by the search engine.
|
|
|
|
Supported natural language sentences and patterns for queries are:
|
|
· MODIFIED TODAY
|
|
· MODIFIED YESTERDAY
|
|
· MODIFIED THIS [ DAY | WEEK | MONTH | YEAR ]
|
|
· LAST <NUMBER> [ DAYS | WEEKS | MONTHS | YEARS ]
|
|
· <NUMBER> [ DAYS | WEEKS | MONTHS | YEARS ] AGO
|
|
|
|
<NUMBER> can be any number or a number text from ONE to TWENTY.
|
|
|
|
Remarks: LAST DAY, if used, is interpreted as YESTERDAY.
|
|
|
|
Supported expressions for --exclude and --recursive-exclude are:
|
|
· width<CMP_OP>height - only if file has width and height properties
|
|
· height<CMP_OP>width - only if file has width and height properties
|
|
· PORTRAIT - only if file width is greater or equal to height
|
|
· LANDSCAPE - only if file height is greater or equal to width
|
|
· SQUARE - only if file width equals to height
|
|
|
|
<CMP_OP> can be: != | >= | <= | = | > | <"""
|
|
print(help_query)
|
|
|
|
|
|
def print_version() -> None:
|
|
"""Prints version information."""
|
|
print(f"{PROG_NAME} v{PROG_VERSION} - {PROG_DATE}")
|
|
print(
|
|
f"Copyright (C) {PROG_DATE[:4]} by {PROG_BY} and, mostly, "
|
|
"the good people at KDE"
|
|
)
|
|
|
|
|
|
def signal_handler(sig, frame) -> None:
|
|
"""Handles Ctrl+C gracefully."""
|
|
print("\nSearch canceled at user request.")
|
|
sys.exit(0)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="An improved search tool for Baloo"
|
|
)
|
|
parser.add_argument("query", nargs="?", help="list of words to query for")
|
|
parser.add_argument("-d", "--directory", help="limit search to specified directory")
|
|
parser.add_argument("-e", "--exclude", help="Search exclude pattern")
|
|
parser.add_argument("-i", "--id", action="store_true", help="show document IDs")
|
|
parser.add_argument("-k", "--konsole", action="store_true", help="show files using file:/ and quotes")
|
|
parser.add_argument("-l", "--limit", type=int, help="the maximum number of results")
|
|
parser.add_argument("-o", "--offset", type=int, help="offset from which to start the search")
|
|
parser.add_argument("-r", "--recursive", nargs="?", const="", default=None, help="enable recurse with or without a query")
|
|
parser.add_argument("-n", "--recursive-indent", help="recursive indent character")
|
|
parser.add_argument("-x", "--recursive-exclude", help="recursion exclude pattern")
|
|
parser.add_argument("-s", "--sort", help="sorting criteria <auto|none>")
|
|
parser.add_argument("-t", "--type", help="type of Baloo data to be searched")
|
|
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose mode")
|
|
|
|
parser.add_argument("--day", type=int, help="day fixed filter, --month is required")
|
|
parser.add_argument("--month", type=int, help="month fixed filter, --year is required")
|
|
parser.add_argument("--year", type=int, help="year filter fixed filter")
|
|
|
|
parser.add_argument("--help-query", action="store_true", help="show query syntax help")
|
|
parser.add_argument("--version", action="store_true", help="show version information")
|
|
|
|
args, unknown_args = parser.parse_known_args()
|
|
|
|
query_parts = [args.query] if args.query else []
|
|
if unknown_args:
|
|
query_parts.extend(unknown_args)
|
|
|
|
query_text = " ".join(query_parts)
|
|
|
|
if args.day is not None and args.month is None:
|
|
raise ValueError("Missing --month (required when --day is used)")
|
|
|
|
if args.month is not None and args.year is None:
|
|
raise ValueError("Missing --year (requered when --month is used)")
|
|
|
|
if args.help_query:
|
|
print_help_query()
|
|
return
|
|
|
|
if args.version:
|
|
print_version()
|
|
return
|
|
|
|
if not query_text and not args.recursive and not args.type and not args.directory:
|
|
parser.print_help()
|
|
return
|
|
|
|
# Configuration and Sort restoring
|
|
user_config = load_config()
|
|
if args.sort:
|
|
user_config["last_sort_order"] = args.sort
|
|
save_config(user_config)
|
|
elif "last_sort_order" in user_config:
|
|
args.sort = user_config["last_sort_order"]
|
|
|
|
# Build options dictionary
|
|
main_options = {}
|
|
if args.recursive is not None:
|
|
main_options["type"] = "folder"
|
|
else:
|
|
if args.limit is not None:
|
|
main_options["limit"] = args.limit
|
|
if args.offset is not None:
|
|
main_options["offset"] = args.offset
|
|
if args.type:
|
|
main_options["type"] = args.type
|
|
|
|
if args.directory:
|
|
main_options["directory"] = args.directory
|
|
if args.year is not None:
|
|
main_options["year"] = args.year
|
|
if args.month is not None:
|
|
main_options["month"] = args.month
|
|
if args.day is not None:
|
|
main_options["day"] = args.day
|
|
if args.sort:
|
|
main_options["sort"] = args.sort
|
|
|
|
other_options = {
|
|
"exclude": args.exclude,
|
|
"id": args.id,
|
|
"konsole": args.konsole,
|
|
"limit": args.limit if args.limit and args.recursive is not None else 99999999999,
|
|
"offset": args.offset if args.offset and args.recursive is not None else 0,
|
|
"recursive": args.recursive,
|
|
"recursive_indent": args.recursive_indent or "",
|
|
"recursive_exclude": args.recursive_exclude,
|
|
"sort": args.sort,
|
|
"type": args.type if args.recursive is not None else None,
|
|
"verbose": args.verbose,
|
|
}
|
|
|
|
if other_options["verbose"]:
|
|
print(f"Query: '{query_text}'")
|
|
print(f"Main Options: {main_options}")
|
|
print(f"Other Options: {other_options}")
|
|
print("-" * 30)
|
|
|
|
try:
|
|
searcher = BagheeraSearcher()
|
|
files_count = 0
|
|
|
|
# Consumir el generador de la librería
|
|
for item in searcher.search(query_text, main_options, other_options):
|
|
if other_options["konsole"]:
|
|
output = f"file:/'{item['path']}'"
|
|
else:
|
|
output = item["path"]
|
|
|
|
if other_options["id"]:
|
|
output += f" [ID: {item['id']}]"
|
|
|
|
print(output)
|
|
files_count += 1
|
|
|
|
if other_options["verbose"]:
|
|
if files_count == 0:
|
|
print("No results found.")
|
|
else:
|
|
print(f"Total: {files_count} files found.")
|
|
|
|
except FileNotFoundError as e:
|
|
print(e)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"Error executing search: {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
try:
|
|
main()
|
|
except Exception as e:
|
|
print(f"Critical error: {e}")
|
|
sys.exit(1)
|