Added verbose to PhotosDB(), partial fix for #110

This commit is contained in:
Rhet Turnbull
2020-10-25 08:16:54 -07:00
parent 667c89e32c
commit d87b8f30a4
6 changed files with 170 additions and 333 deletions

View File

@@ -521,10 +521,15 @@ def cli(ctx, db, json_, debug):
help="Use with '--dump photos' to dump only certain UUIDs", help="Use with '--dump photos' to dump only certain UUIDs",
multiple=True, multiple=True,
) )
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
@click.pass_obj @click.pass_obj
@click.pass_context @click.pass_context
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid): def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose_):
""" Print out debug info """ """ Print out debug info """
global VERBOSE
VERBOSE = bool(verbose_)
db = get_photos_db(*photos_library, db, cli_obj.db) db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None: if db is None:
click.echo(cli.commands["debug-dump"].get_help(ctx), err=True) click.echo(cli.commands["debug-dump"].get_help(ctx), err=True)
@@ -534,7 +539,7 @@ def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid):
start_t = time.perf_counter() start_t = time.perf_counter()
print(f"Opening database: {db}") print(f"Opening database: {db}")
photosdb = osxphotos.PhotosDB(dbfile=db) photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose)
stop_t = time.perf_counter() stop_t = time.perf_counter()
print(f"Done; took {(stop_t-start_t):.2f} seconds") print(f"Done; took {(stop_t-start_t):.2f} seconds")
@@ -1900,7 +1905,7 @@ def _query(
arguments must be passed in same order as query and export arguments must be passed in same order as query and export
if either is modified, need to ensure all three functions are updated """ if either is modified, need to ensure all three functions are updated """
photosdb = osxphotos.PhotosDB(dbfile=db) photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose)
if deleted or deleted_only: if deleted or deleted_only:
photos = photosdb.photos( photos = photosdb.photos(
uuid=uuid, uuid=uuid,

View File

@@ -1,4 +1,4 @@
""" version info """ """ version info """
__version__ = "0.35.8" __version__ = "0.35.9"

View File

@@ -44,6 +44,7 @@ from ..utils import (
_get_os_version, _get_os_version,
_open_sql_file, _open_sql_file,
get_last_library_path, get_last_library_path,
noop,
normalize_unicode, normalize_unicode,
) )
from .photosdb_utils import get_db_model_version, get_db_version from .photosdb_utils import get_db_model_version, get_db_version
@@ -68,11 +69,16 @@ class PhotosDB:
) )
from ._photosdb_process_scoreinfo import _process_scoreinfo from ._photosdb_process_scoreinfo import _process_scoreinfo
def __init__(self, dbfile=None): def __init__(self, dbfile=None, verbose=None):
""" Create a new PhotosDB object. """ Create a new PhotosDB object.
Args: Args:
dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos. dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos.
verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.
Raises:
FileNotFoundError if dbfile is not a valid Photos library.
TypeError if verbose is not None and not callable.
""" """
# Check OS version # Check OS version
@@ -85,6 +91,12 @@ class PhotosDB:
f"you have {system}, OS version: {major}" f"you have {system}, OS version: {major}"
) )
if verbose is None:
verbose = noop
elif not callable(verbose):
raise TypeError("verbose must be callable")
self._verbose = verbose
# create a temporary directory # create a temporary directory
# tempfile.TemporaryDirectory gets cleaned up when the object does # tempfile.TemporaryDirectory gets cleaned up when the object does
self._tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_") self._tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
@@ -245,11 +257,14 @@ class PhotosDB:
# or photosanalysisd # or photosanalysisd
self._dbfile = self._dbfile_actual = self._tmp_db = os.path.abspath(dbfile) self._dbfile = self._dbfile_actual = self._tmp_db = os.path.abspath(dbfile)
verbose(f"Processing database {self._dbfile}")
# if database is exclusively locked, make a copy of it and use the copy # if database is exclusively locked, make a copy of it and use the copy
# Photos maintains an exclusive lock on the database file while Photos is open # Photos maintains an exclusive lock on the database file while Photos is open
# photoanalysisd sometimes maintains this lock even after Photos is closed # photoanalysisd sometimes maintains this lock even after Photos is closed
# In those cases, make a temp copy of the file for sqlite3 to read # In those cases, make a temp copy of the file for sqlite3 to read
if _db_is_locked(self._dbfile): if _db_is_locked(self._dbfile):
verbose(f"Database locked, creating temporary copy.")
self._tmp_db = self._copy_db_file(self._dbfile) self._tmp_db = self._copy_db_file(self._dbfile)
self._db_version = get_db_version(self._tmp_db) self._db_version = get_db_version(self._tmp_db)
@@ -262,8 +277,10 @@ class PhotosDB:
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile) raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
else: else:
self._dbfile_actual = self._tmp_db = dbfile self._dbfile_actual = self._tmp_db = dbfile
verbose(f"Processing database {self._dbfile_actual}")
# if database is exclusively locked, make a copy of it and use the copy # if database is exclusively locked, make a copy of it and use the copy
if _db_is_locked(self._dbfile_actual): if _db_is_locked(self._dbfile_actual):
verbose(f"Database locked, creating temporary copy.")
self._tmp_db = self._copy_db_file(self._dbfile_actual) self._tmp_db = self._copy_db_file(self._dbfile_actual)
if _debug(): if _debug():
@@ -532,10 +549,15 @@ class PhotosDB:
""" process the Photos database to extract info """ process the Photos database to extract info
works on Photos version <= 4.0 """ works on Photos version <= 4.0 """
verbose = self._verbose
verbose("Processing database.")
verbose(f"Database version: {self._db_version}.")
(conn, c) = _open_sql_file(self._tmp_db) (conn, c) = _open_sql_file(self._tmp_db)
# get info to associate persons with photos # get info to associate persons with photos
# then get detected faces in each photo and link to persons # then get detected faces in each photo and link to persons
verbose("Processing persons in photos.")
c.execute( c.execute(
""" SELECT """ SELECT
RKPerson.modelID, RKPerson.modelID,
@@ -601,6 +623,7 @@ class PhotosDB:
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]") logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
# get information on detected faces # get information on detected faces
verbose("Processing detected faces in photos.")
c.execute( c.execute(
""" SELECT """ SELECT
RKPerson.modelID, RKPerson.modelID,
@@ -638,6 +661,7 @@ class PhotosDB:
logging.debug(pformat(self._dbfaces_uuid)) logging.debug(pformat(self._dbfaces_uuid))
# Get info on albums # Get info on albums
verbose("Processing albums.")
c.execute( c.execute(
""" SELECT """ SELECT
RKAlbum.uuid, RKAlbum.uuid,
@@ -780,6 +804,7 @@ class PhotosDB:
logging.debug(pformat(self._dbfolder_details)) logging.debug(pformat(self._dbfolder_details))
# Get info on keywords # Get info on keywords
verbose("Processing keywords.")
c.execute( c.execute(
""" SELECT """ SELECT
RKKeyword.name, RKKeyword.name,
@@ -807,6 +832,7 @@ class PhotosDB:
self._dbvolumes[vol[0]] = vol[1] self._dbvolumes[vol[0]] = vol[1]
# Get photo details # Get photo details
verbose("Processing photo details.")
if self._db_version < _PHOTOS_3_VERSION: if self._db_version < _PHOTOS_3_VERSION:
# Photos < 3.0 doesn't have RKVersion.selfPortrait (selfie) # Photos < 3.0 doesn't have RKVersion.selfPortrait (selfie)
c.execute( c.execute(
@@ -1096,6 +1122,7 @@ class PhotosDB:
self._dbphotos[uuid]["fok_import_session"] = None self._dbphotos[uuid]["fok_import_session"] = None
# get additional details from RKMaster, needed for RAW processing # get additional details from RKMaster, needed for RAW processing
verbose("Processing additional photo details.")
c.execute( c.execute(
""" SELECT """ SELECT
RKMaster.uuid, RKMaster.uuid,
@@ -1269,6 +1296,7 @@ class PhotosDB:
self._dbphotos[uuid]["incloud"] = True if row[2] == 1 else False self._dbphotos[uuid]["incloud"] = True if row[2] == 1 else False
# get location data # get location data
verbose("Processing location data.")
# get the country codes # get the country codes
country_codes = c.execute( country_codes = c.execute(
"SELECT modelID, countryCode " "SELECT modelID, countryCode "
@@ -1355,6 +1383,7 @@ class PhotosDB:
conn.close() conn.close()
# process faces # process faces
verbose("Processing face details.")
self._process_faceinfo() self._process_faceinfo()
# add faces and keywords to photo data # add faces and keywords to photo data
@@ -1391,6 +1420,7 @@ class PhotosDB:
self._dbphotos[uuid]["volume"] = None self._dbphotos[uuid]["volume"] = None
# done processing, dump debug data if requested # done processing, dump debug data if requested
verbose("Done processing details from Photos library.")
if _debug(): if _debug():
logging.debug("Faces (_dbfaces_uuid):") logging.debug("Faces (_dbfaces_uuid):")
logging.debug(pformat(self._dbfaces_uuid)) logging.debug(pformat(self._dbfaces_uuid))
@@ -1466,12 +1496,14 @@ class PhotosDB:
if _debug(): if _debug():
logging.debug(f"_process_database5") logging.debug(f"_process_database5")
verbose = self._verbose
verbose(f"Processing database.")
(conn, c) = _open_sql_file(self._tmp_db) (conn, c) = _open_sql_file(self._tmp_db)
# some of the tables/columns have different names in different versions of Photos # some of the tables/columns have different names in different versions of Photos
photos_ver = get_db_model_version(self._tmp_db) photos_ver = get_db_model_version(self._tmp_db)
self._photos_ver = photos_ver self._photos_ver = photos_ver
verbose(f"Database version: {self._db_version}, {photos_ver}.")
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"] asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"] keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
album_join = _DB_TABLE_NAMES[photos_ver]["ALBUM_JOIN"] album_join = _DB_TABLE_NAMES[photos_ver]["ALBUM_JOIN"]
@@ -1485,6 +1517,7 @@ class PhotosDB:
# get info to associate persons with photos # get info to associate persons with photos
# then get detected faces in each photo and link to persons # then get detected faces in each photo and link to persons
verbose("Processing persons in photos.")
c.execute( c.execute(
""" SELECT """ SELECT
ZPERSON.Z_PK, ZPERSON.Z_PK,
@@ -1550,6 +1583,7 @@ class PhotosDB:
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]") logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
# get information on detected faces # get information on detected faces
verbose("Processing detected faces in photos.")
c.execute( c.execute(
f""" SELECT f""" SELECT
ZPERSON.Z_PK, ZPERSON.Z_PK,
@@ -1584,6 +1618,7 @@ class PhotosDB:
logging.debug(pformat(self._dbfaces_uuid)) logging.debug(pformat(self._dbfaces_uuid))
# get details about albums # get details about albums
verbose("Processing albums.")
c.execute( c.execute(
f""" SELECT f""" SELECT
ZGENERICALBUM.ZUUID, ZGENERICALBUM.ZUUID,
@@ -1702,6 +1737,7 @@ class PhotosDB:
logging.debug(pformat(self._dbalbum_folders)) logging.debug(pformat(self._dbalbum_folders))
# get details on keywords # get details on keywords
verbose("Processing keywords.")
c.execute( c.execute(
f"""SELECT ZKEYWORD.ZTITLE, {asset_table}.ZUUID f"""SELECT ZKEYWORD.ZTITLE, {asset_table}.ZUUID
FROM {asset_table} FROM {asset_table}
@@ -1733,6 +1769,7 @@ class PhotosDB:
logging.debug(self._dbvolumes) logging.debug(self._dbvolumes)
# get details about photos # get details about photos
verbose("Processing photo details.")
logging.debug(f"Getting information about photos") logging.debug(f"Getting information about photos")
c.execute( c.execute(
f"""SELECT {asset_table}.ZUUID, f"""SELECT {asset_table}.ZUUID,
@@ -1891,7 +1928,7 @@ class PhotosDB:
info["type"] = None info["type"] = None
info["UTI"] = row[18] info["UTI"] = row[18]
info["UTI_original"] = None # filled in later info["UTI_original"] = None # filled in later
# handle burst photos # handle burst photos
# if burst photo, determine whether or not it's a selected burst photo # if burst photo, determine whether or not it's a selected burst photo
@@ -2023,6 +2060,7 @@ class PhotosDB:
# 1 ZGENERICASSET.ZIMPORTSESSION # 1 ZGENERICASSET.ZIMPORTSESSION
# 2 ZGENERICASSET.Z_FOK_IMPORTSESSION # 2 ZGENERICASSET.Z_FOK_IMPORTSESSION
# 3 ZGENERICALBUM.ZUUID, # 3 ZGENERICALBUM.ZUUID,
verbose("Processing import sessions.")
c.execute( c.execute(
f"""SELECT f"""SELECT
{asset_table}.ZUUID, {asset_table}.ZUUID,
@@ -2045,6 +2083,7 @@ class PhotosDB:
logging.debug(f"No info record for uuid {uuid} for import session") logging.debug(f"No info record for uuid {uuid} for import session")
# Get extended description # Get extended description
verbose("Processing additional photo details.")
c.execute( c.execute(
f"""SELECT {asset_table}.ZUUID, f"""SELECT {asset_table}.ZUUID,
ZASSETDESCRIPTION.ZLONGDESCRIPTION ZASSETDESCRIPTION.ZLONGDESCRIPTION
@@ -2224,18 +2263,23 @@ class PhotosDB:
conn.close() conn.close()
# process face info # process face info
verbose("Processing face details.")
self._process_faceinfo() self._process_faceinfo()
# process search info # process search info
verbose("Processing photo labels.")
self._process_searchinfo() self._process_searchinfo()
# process exif info # process exif info
verbose("Processing EXIF details.")
self._process_exifinfo() self._process_exifinfo()
# process computed scores # process computed scores
verbose("Processing computed aesthetic scores.")
self._process_scoreinfo() self._process_scoreinfo()
# done processing, dump debug data if requested # done processing, dump debug data if requested
verbose("Done processing details from Photos library.")
if _debug(): if _debug():
logging.debug("Faces (_dbfaces_uuid):") logging.debug("Faces (_dbfaces_uuid):")
logging.debug(pformat(self._dbfaces_uuid)) logging.debug(pformat(self._dbfaces_uuid))

View File

@@ -57,6 +57,9 @@ def _debug():
""" returns True if debugging turned on (via _set_debug), otherwise, false """ """ returns True if debugging turned on (via _set_debug), otherwise, false """
return _DEBUG return _DEBUG
def noop(*args, **kwargs):
""" do nothing (no operation) """
pass
def _get_os_version(): def _get_os_version():
# returns tuple containing OS version # returns tuple containing OS version

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ import os
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
import osxphotos
from osxphotos.exiftool import get_exiftool_path from osxphotos.exiftool import get_exiftool_path
CLI_PHOTOS_DB = "tests/Test-10.15.1.photoslibrary" CLI_PHOTOS_DB = "tests/Test-10.15.1.photoslibrary"
@@ -419,6 +420,13 @@ CLI_EXPORT_UUID_FROM_FILE_FILENAMES = [
] ]
@pytest.fixture(autouse=True)
def reset_globals():
""" reset globals in __main__ that tests may have changed """
yield
osxphotos.__main__.VERBOSE = False
# determine if exiftool installed so exiftool tests can be skipped # determine if exiftool installed so exiftool tests can be skipped
try: try:
exiftool = get_exiftool_path() exiftool = get_exiftool_path()