feat(images): store images' own categories in the library database (#332)
One new table, image_categories, and its category index, created empty by a forward-only migration that reads and writes only its own objects. Startup checks the structure; restore also checks the values. The store sets, clears and reads own categories by file key (the deepest output source holding the file and the path below it), and copies a removed duplicate's category to a kept copy that has none.
This commit is contained in:
@@ -800,6 +800,49 @@ def _favorite_root_key(root) -> str:
|
||||
return _favorite_relative_key(os.path.realpath(str(root)))
|
||||
|
||||
|
||||
def _source_depth(source: str) -> int:
|
||||
return len([part for part in re.split(r"[\\/]+", source) if part])
|
||||
|
||||
|
||||
def deepest_source(sources, absolute_path):
|
||||
"""``(source, POSIX path below it)`` of the deepest source holding the path.
|
||||
|
||||
*sources* are compared by their real path (they are stored resolved); the
|
||||
path is compared lexically after ``normpath`` and never resolved, so a
|
||||
symlinked subfolder keeps its listed spelling. Containment folds case
|
||||
where the OS does. None when no source contains the path.
|
||||
"""
|
||||
path = os.path.normpath(str(absolute_path))
|
||||
folded = os.path.normcase(path)
|
||||
best = None
|
||||
for source in sources:
|
||||
real = os.path.normpath(os.path.realpath(str(source)))
|
||||
prefix = os.path.join(os.path.normcase(real), "")
|
||||
if not folded.startswith(prefix):
|
||||
continue
|
||||
if best is None or _source_depth(real) > _source_depth(best):
|
||||
best = real
|
||||
if best is None:
|
||||
return None
|
||||
relative = os.path.relpath(path, best)
|
||||
return best, relative.replace(os.sep, "/")
|
||||
|
||||
|
||||
def image_category_key(sources, absolute_path):
|
||||
"""The file key of an image's own category (#332), or None.
|
||||
|
||||
``(root key, relative key)``: the deepest output source that contains the
|
||||
file and the path below it, keyed with the favorites helpers. Without
|
||||
nested sources it equals the file's favorite key. A path no source
|
||||
contains has no key (and so no own category).
|
||||
"""
|
||||
found = deepest_source(sources, absolute_path)
|
||||
if found is None:
|
||||
return None
|
||||
source, relative = found
|
||||
return _favorite_root_key(source), _favorite_relative_key(relative)
|
||||
|
||||
|
||||
def _name_suffix(name):
|
||||
"""``Path(name).suffix`` as Python 3.10-3.13 compute it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user