Files
BagheeraSearch/bagheerasearch.py
Ignacio Serantes 6207cab27a v1.1.0
2026-05-09 10:26:57 +02:00

334 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
# flake8: noqa: E501
"""
Bagheera Search Tool - CLI Client
"""
__appname__ = "BagheeraSearch"
__version__ = "1.1"
__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 = __version__
PROG_BY = __author__
PROG_DATE = "2026-05-09"
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 logical operators 'AND' or 'OR' and additional parenthesis, but note that 'NOT' logical operator is not available.
The full list of properties which can be searched is listed below. They are grouped by file types.
All Files
· filename
· mimetype
· modified
· rating
· tags
· userComment
Audio
· Album
· AlbumArtist
· Artist
· BitRate
· Channels
· Comment
· Composer
· Duration
· Genre
· Lyricist
· ReleaseYear
· SampleRate
· TrackNumber
Documents
· Author
· Copyright
· CreationDate
· Generator
· Keywords
· Language
· LineCount
· PageCount
· Publisher
· Subject
· Title
· WordCount
Media
· AspectRatio
· FrameRate
· Height
· ImageDateTime
· ImageMake
· ImageModel
· ImageOrientation
· Images
· PhotoApertureValue
· PhotoDateTimeOriginal
· PhotoExposureBiasValue
· PhotoExposureTime
· PhotoFlash
· PhotoFNumber
· PhotoFocalLength
· PhotoFocalLengthIn35mmFilm
· PhotoGpsAltitude
· PhotoGpsLatitude
· PhotoGpsLongitude
· PhotoISOSpeedRatings
· PhotoMeteringMode
· PhotoPixelXDimension
· PhotoPixelYDimension
· PhotoSaturation
· PhotoSharpness
· PhotoWhiteBalance
· Width
{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 ]
· MODIFIED LAST <NUMBER> [ DAYS | WEEKS | MONTHS | YEARS ]
· MODIFIED <NUMBER> [ DAYS | WEEKS | MONTHS | YEARS ] AGO
<NUMBER> can be any number or a number text from ONE to TWENTY.
The --exclude and --recursive-exclude options allow you to filter files out of the results. The syntax for both options supports parentheses and logical operators (AND, OR, and NOT) to combine multiple patterns.
In addition to standard query comparison operators, the not equal (!=) operator is available for comparing properties against specific values. Furthermore, you can compare two properties directly; for example, 'width > height' is a valid expression.
Remarks:
· All text comparison are case insensitive.
· Tags comparisons are performed against both individual full tag string (using the '/' character as a level separator) and each individual level. All individual level values are normalized to lowercase and stripped of accents or diacritics. For example, a file tagged as 'Opera,Person/María Callas,Singer' would match any of the following elements: ['Opera', 'Person/María Callas', 'Singer', 'callas', 'maria', 'opera', 'person', 'singer']."
· Only text and numeric data are supported."""
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 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 (required 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)