@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Constants used by osxphotos
|
||||
"""
|
||||
""" Constants used by osxphotos """
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
from datetime import datetime
|
||||
@@ -138,7 +138,9 @@ _PHOTOS_5_SHARED_PHOTO_PATH = "resources/cloudsharing/data"
|
||||
_PHOTOS_8_SHARED_PHOTO_PATH = "scopes/cloudsharing/data"
|
||||
|
||||
# Where are shared iCloud derivatives located?
|
||||
_PHOTOS_5_SHARED_DERIVATIVE_PATH = "resources/cloudsharing/resources/derivatives/masters"
|
||||
_PHOTOS_5_SHARED_DERIVATIVE_PATH = (
|
||||
"resources/cloudsharing/resources/derivatives/masters"
|
||||
)
|
||||
_PHOTOS_8_SHARED_DERIVATIVE_PATH = "scopes/cloudsharing/resources/derivatives/masters"
|
||||
|
||||
# What type of file? Based on ZGENERICASSET.ZKIND in Photos 5 database
|
||||
@@ -213,11 +215,11 @@ class SearchCategory:
|
||||
TITLE = 2017
|
||||
DESCRIPTION = 2018
|
||||
HOME = 2020
|
||||
WORK = 2036
|
||||
PERSON = 2021
|
||||
ACTIVITY = 2027
|
||||
HOLIDAY = 2029
|
||||
SEASON = 2030
|
||||
WORK = 2036
|
||||
VENUE = 2038
|
||||
VENUE_TYPE = 2039
|
||||
PHOTO_TYPE_VIDEO = 2044
|
||||
@@ -230,6 +232,7 @@ class SearchCategory:
|
||||
PHOTO_TYPE_PORTRAIT = 2053
|
||||
PHOTO_TYPE_SELFIES = 2054
|
||||
PHOTO_TYPE_FAVORITES = 2055
|
||||
PHOTO_TYPE_ANIMATED = None # Photos 8+ only
|
||||
MEDIA_TYPES = [
|
||||
PHOTO_TYPE_VIDEO,
|
||||
PHOTO_TYPE_SLOMO,
|
||||
@@ -244,7 +247,23 @@ class SearchCategory:
|
||||
]
|
||||
PHOTO_NAME = 2056
|
||||
CAMERA = None # Photos 8+ only
|
||||
TEXT_FOUND = None # Photos 8+ only
|
||||
DETECTED_TEXT = None # Photos 8+ only
|
||||
SOURCE = None # Photos 8+ only
|
||||
|
||||
@classmethod
|
||||
def categories(cls) -> dict[int, str]:
|
||||
"""Return categories as dict of value: name"""
|
||||
# a bit of a hack to basically reverse the enum
|
||||
return {
|
||||
value: name
|
||||
for name, value in cls.__dict__.items()
|
||||
if name is not None
|
||||
and not name.startswith("__")
|
||||
and not callable(name)
|
||||
and name.isupper()
|
||||
and not isinstance(value, (list, dict, tuple))
|
||||
}
|
||||
|
||||
|
||||
class SearchCategory_Photos8(SearchCategory):
|
||||
@@ -252,6 +271,20 @@ class SearchCategory_Photos8(SearchCategory):
|
||||
|
||||
# Many of the category values changed in Ventura / Photos 8
|
||||
# and some new categories were added
|
||||
CITY = 5
|
||||
LOCALITY_4 = 4
|
||||
SUB_LOCALITY_5 = None
|
||||
SUB_LOCALITY_6 = 6
|
||||
LOCALITY_8 = 8
|
||||
NAMED_AREA = 7
|
||||
ALL_LOCALITY = [
|
||||
LOCALITY_4,
|
||||
SUB_LOCALITY_6,
|
||||
LOCALITY_8,
|
||||
NAMED_AREA,
|
||||
]
|
||||
HOME = 1000
|
||||
WORK = 1001
|
||||
LABEL = 1500
|
||||
MONTH = 1100
|
||||
YEAR = 1101
|
||||
@@ -261,11 +294,55 @@ class SearchCategory_Photos8(SearchCategory):
|
||||
TITLE = 1201
|
||||
DESCRIPTION = 1202
|
||||
DETECTED_TEXT = 1203 # new in Photos 8
|
||||
TEXT_FOUND = 1205 # new in Photos 8
|
||||
PERSON = 1300
|
||||
ACTIVITY = 1600
|
||||
VENUE = 1700
|
||||
VENUE_TYPE = 1701
|
||||
PHOTO_TYPE_VIDEO = 1901
|
||||
PHOTO_TYPE_SELFIES = 1915
|
||||
PHOTO_TYPE_LIVE = 1906
|
||||
PHOTO_TYPE_PORTRAIT = 1914
|
||||
PHOTO_TYPE_FAVORITES = 2000
|
||||
PHOTO_TYPE_PANORAMA = 1908
|
||||
PHOTO_TYPE_TIMELAPSE = 1909
|
||||
PHOTO_TYPE_SLOMO = 1905
|
||||
PHOTO_TYPE_BURSTS = 1913
|
||||
PHOTO_TYPE_SCREENSHOT = 1907
|
||||
PHOTO_TYPE_ANIMATED = 1912
|
||||
PHOTO_TYPE_RAW = 1902
|
||||
MEDIA_TYPES = [
|
||||
PHOTO_TYPE_VIDEO,
|
||||
PHOTO_TYPE_SLOMO,
|
||||
PHOTO_TYPE_LIVE,
|
||||
PHOTO_TYPE_SCREENSHOT,
|
||||
PHOTO_TYPE_PANORAMA,
|
||||
PHOTO_TYPE_TIMELAPSE,
|
||||
PHOTO_TYPE_BURSTS,
|
||||
PHOTO_TYPE_PORTRAIT,
|
||||
PHOTO_TYPE_SELFIES,
|
||||
PHOTO_TYPE_FAVORITES,
|
||||
PHOTO_TYPE_ANIMATED,
|
||||
]
|
||||
PHOTO_NAME = 2100
|
||||
CAMERA = 2300 # new in Photos 8
|
||||
SOURCE = 2200 # new in Photos 8, shows the app/software source for the photo, e.g. Messages, Safari, etc.
|
||||
|
||||
@classmethod
|
||||
def categories(cls) -> dict[int, str]:
|
||||
"""Return categories as dict of value: name"""
|
||||
# need to get the categories from the base class and update with the new values
|
||||
classdict = SearchCategory.__dict__.copy()
|
||||
classdict |= cls.__dict__.copy()
|
||||
return {
|
||||
value: name
|
||||
for name, value in classdict.items()
|
||||
if name is not None
|
||||
and not name.startswith("__")
|
||||
and not callable(name)
|
||||
and name.isupper()
|
||||
and not isinstance(value, (list, dict, tuple))
|
||||
}
|
||||
|
||||
|
||||
def search_category_factory(version: int) -> SearchCategory:
|
||||
|
||||
@@ -21,7 +21,7 @@ from rich.live import Live
|
||||
from rich.panel import Panel
|
||||
|
||||
from osxphotos import PhotoInfo, PhotosDB
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
from osxphotos._constants import _UNKNOWN_PERSON, search_category_factory
|
||||
from osxphotos.rich_utils import add_rich_markup_tag
|
||||
from osxphotos.text_detection import detect_text as detect_text_in_photo
|
||||
from osxphotos.utils import dd_to_dms_str
|
||||
@@ -65,6 +65,22 @@ def trim(text: str, pad: str = "") -> str:
|
||||
return text if len(text) <= width else f"{text[: width- 3]}..."
|
||||
|
||||
|
||||
def format_search_info(photo: PhotoInfo) -> str:
|
||||
"""Format search info for photo"""
|
||||
categories = sorted(list(photo._db._db_searchinfo_categories.keys()))
|
||||
search_info = photo.search_info
|
||||
if not search_info:
|
||||
return ""
|
||||
search_info_strs = []
|
||||
category_dict = search_category_factory(photo._db.photos_version).categories()
|
||||
for category in categories:
|
||||
if text := search_info._get_text_for_category(category):
|
||||
text = ", ".join(t for t in text if t) if isinstance(text, list) else text
|
||||
category_name = str(category_dict.get(category, category)).lower()
|
||||
search_info_strs.append(f"{bold(category_name)}: {text}")
|
||||
return ", ".join(search_info_strs)
|
||||
|
||||
|
||||
def inspect_photo(
|
||||
photo: PhotoInfo,
|
||||
detected_text: Optional[str] = None,
|
||||
@@ -138,8 +154,10 @@ def inspect_photo(
|
||||
+ f"{', '.join(dd_to_dms_str(*photo.location)) if photo.location[0] else '-'}",
|
||||
bold("Place: ") + f"{photo.place.name if photo.place else '-'}",
|
||||
bold("Categories/Labels: ") + f"{', '.join(photo.labels) or '-'}",
|
||||
bold("Search Info: ") + format_search_info(photo),
|
||||
]
|
||||
)
|
||||
|
||||
properties.append(format_flags(photo))
|
||||
properties.append(format_albums(photo))
|
||||
|
||||
|
||||
@@ -324,8 +324,15 @@ class PhotosDB:
|
||||
# _db_version is set from photos.db
|
||||
self._db_version = get_db_version(self._tmp_db)
|
||||
# _photos_version is set from Photos.sqlite which only exists for Photos 5+
|
||||
self._photos_ver = 4 if self._db_version == 4 else 5
|
||||
|
||||
db_ver_int = int(self._db_version)
|
||||
if db_ver_int < 3000:
|
||||
self._photos_ver = 2
|
||||
elif db_ver_int < 4000:
|
||||
self._photos_ver = 3
|
||||
elif db_ver_int < 5000:
|
||||
self._photos_ver = 4
|
||||
else:
|
||||
self._photos_ver = 5
|
||||
# If Photos >= 5, actual data isn't in photos.db but in Photos.sqlite
|
||||
if int(self._db_version) > int(_PHOTOS_4_VERSION):
|
||||
dbpath = pathlib.Path(self._dbfile).parent
|
||||
@@ -585,6 +592,11 @@ class PhotosDB:
|
||||
"""returns path to the Photos library PhotosDB was initialized with"""
|
||||
return self._library_path
|
||||
|
||||
@property
|
||||
def photos_version(self):
|
||||
"""returns version of Photos app that created the library"""
|
||||
return self._photos_ver
|
||||
|
||||
def get_db_connection(self):
|
||||
"""Get connection to the working copy of the Photos database
|
||||
|
||||
|
||||
@@ -139,6 +139,13 @@ class SearchInfo:
|
||||
return []
|
||||
return self._get_text_for_category(self._categories.DETECTED_TEXT)
|
||||
|
||||
@property
|
||||
def text_found(self):
|
||||
"""Returns True if photos has detected text (macOS 13+ / Photos 8+ only)"""
|
||||
if self._photo._db._photos_ver < 8:
|
||||
return []
|
||||
return self._get_text_for_category(self._categories.TEXT_FOUND)
|
||||
|
||||
@property
|
||||
def camera(self):
|
||||
"""returns camera name (macOS 13+ / Photos 8+ only)"""
|
||||
@@ -147,10 +154,18 @@ class SearchInfo:
|
||||
camera = self._get_text_for_category(self._categories.CAMERA)
|
||||
return camera[0] if camera else ""
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""returns source of the photo (e.g. "Messages", "Safar", etc) (macOS 13+ / Photos 8+ only)"""
|
||||
if self._photo._db._photos_ver < 8:
|
||||
return ""
|
||||
source = self._get_text_for_category(self._categories.SOURCE)
|
||||
return source[0] if source else ""
|
||||
|
||||
@property
|
||||
def all(self):
|
||||
"""return all search info properties in a single list"""
|
||||
all = (
|
||||
all_ = (
|
||||
self.labels
|
||||
+ self.place_names
|
||||
+ self.streets
|
||||
@@ -165,23 +180,23 @@ class SearchInfo:
|
||||
+ self.detected_text
|
||||
)
|
||||
if self.city:
|
||||
all += [self.city]
|
||||
all_ += [self.city]
|
||||
if self.state:
|
||||
all += [self.state]
|
||||
all_ += [self.state]
|
||||
if self.state_abbreviation:
|
||||
all += [self.state_abbreviation]
|
||||
all_ += [self.state_abbreviation]
|
||||
if self.country:
|
||||
all += [self.country]
|
||||
all_ += [self.country]
|
||||
if self.month:
|
||||
all += [self.month]
|
||||
all_ += [self.month]
|
||||
if self.year:
|
||||
all += [self.year]
|
||||
all_ += [self.year]
|
||||
if self.season:
|
||||
all += [self.season]
|
||||
all_ += [self.season]
|
||||
if self.camera:
|
||||
all += [self.camera]
|
||||
all_ += [self.camera]
|
||||
|
||||
return all
|
||||
return all_
|
||||
|
||||
def asdict(self):
|
||||
"""return dict of search info"""
|
||||
@@ -206,6 +221,7 @@ class SearchInfo:
|
||||
"media_types": self.media_types,
|
||||
"detected_text": self.detected_text,
|
||||
"camera": self.camera,
|
||||
"source": self.source,
|
||||
}
|
||||
|
||||
def _get_text_for_category(self, category):
|
||||
|
||||
Reference in New Issue
Block a user