Skip to content

Commit

Permalink
Added --alt-db option to export (#1257)
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull authored Oct 23, 2023
1 parent 72ac665 commit 19a103d
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 7 deletions.
2 changes: 1 addition & 1 deletion examples/import_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
from osxphotos import AlbumInfo, PhotoInfo
from osxphotos.cli import echo, echo_error
from osxphotos.cli.verbose import verbose_print
from osxphotos.photosalbum import PhotosAlbumPhotoScript
from osxphotos.photoquery import QueryOptions
from osxphotos.photosalbum import PhotosAlbumPhotoScript
from osxphotos.utils import pluralize


Expand Down
4 changes: 3 additions & 1 deletion examples/sync_photos_exif_to_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,9 @@ def sync_metadata_for_file(
exif_writer = ExifWriter(photo)
exif_writer.write_exif_data(filepath, options)
if touch:
echo(f"Touching file [filepath]{filepath}[/] to sync Finder dates to EXIF dates")
echo(
f"Touching file [filepath]{filepath}[/] to sync Finder dates to EXIF dates"
)
if not dry_run:
# borrow the touch_files code from PhotoExporter as it does what we need
touch_files(photo, [filepath], ExportOptions(fileutil=FileUtil))
Expand Down
27 changes: 26 additions & 1 deletion osxphotos/cli/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,18 @@
"Unlike the default copy method, --alt-copy does not support "
"copy-on-write on APFS volumes nor does it preserve filesystem metadata.",
)
@click.option(
"--alt-db",
metavar="PATH",
help="Specify alternate path to Photos library database. "
"This is an advanced feature you probably don't need. "
"This may be useful when exporting from a library on a very slow external disk. "
"In this case, you could copy the `/database` folder from the Photos library to the internal disk"
"and use `--alt-db` to specify the path to the database file on the internal disk. "
"then use `--library` to specify the path to the Photos library root on the external disk. "
"For example: `--library /Volumes/ExternalDisk/Photos.photoslibrary --alt-db /path/to/database/Photos.sqlite` ",
type=click.Path(exists=True),
)
@click.option(
"--load-config",
required=False,
Expand Down Expand Up @@ -830,6 +842,7 @@ def export(
added_in_last,
album,
album_keyword,
alt_db,
alt_copy,
append,
beta,
Expand Down Expand Up @@ -1062,6 +1075,7 @@ def export(
added_in_last = cfg.added_in_last
album = cfg.album
album_keyword = cfg.album_keyword
alt_db = cfg.alt_db
alt_copy = cfg.alt_copy
append = cfg.append
beta = cfg.beta
Expand Down Expand Up @@ -1474,12 +1488,23 @@ def export(
query_options = query_options_from_kwargs(**query_kwargs)

if is_iphoto_library(db):
if alt_db:
click.echo("--alt-db is not supported for iPhoto libraries", err=True)
raise click.Abort()
photosdb = osxphotos.iPhotoDB(
dbfile=db, verbose=verbose, exiftool=exiftool_path, rich=False
)
else:
library_path = pathlib.Path(db)
if library_path.is_file():
# get the Photos library path from the database path
library_path = library_path.parent.parent
photosdb = osxphotos.PhotosDB(
dbfile=db, verbose=verbose, exiftool=exiftool_path, rich=True
dbfile=alt_db if alt_db else db,
verbose=verbose,
exiftool=exiftool_path,
rich=True,
library_path=library_path if alt_db else None,
)

# enable beta features if requested
Expand Down
26 changes: 24 additions & 2 deletions osxphotos/photosdb/photosdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def __init__(
verbose=None,
exiftool=None,
rich=None,
library_path=None,
_skip_searchinfo=False,
):
"""Create a new PhotosDB object.
Expand All @@ -114,11 +115,29 @@ def __init__(
verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.
exiftool: optional path to exiftool for methods that require this (e.g. PhotoInfo.exiftool); if not provided, will search PATH
rich: use rich with verbose output
library_path: path to Photos library root if different than where the database is stored; see Notes
_skip_searchinfo: if True, will not process search data from psi.sqlite; useful for processing standalone Photos.sqlite file
Raises:
PhotosDBReadError if dbfile is not a valid Photos library.
TypeError if verbose is not None and not callable.
Notes:
dbfile can be either the path to the Photos library (e.g. ~/Pictures/Photos Library.photoslibrary)
or the path to the photos.db file (e.g. ~/Pictures/Photos Library.photoslibrary/database/photos.db)
or the path to the Photos.sqlite file (e.g. ~/Pictures/Photos Library.photoslibrary/database/Photos.sqlite)
If dbfile is None, will attempt to locate last opened Photos library.
In some cases, it may be useful to copy the database to a different location than the library
(e.g. if the library is on a slow drive or an iPhone).
In that case, set dbfile to the path to the database and set library_path to the path to the library root.
For example:
```python
photosdb = PhotosDB(dbfile="/path/to/database/Photos.sqlite", library_path="/path/to/Photos Library.photoslibrary")
```
If library_path is not provided, PhotosDB determine the library path from the database path. In most cases you should
not provide the library_path argument.
"""

# Check that we're not trying to open an iPhoto library
Expand Down Expand Up @@ -373,8 +392,11 @@ def __init__(
f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}"
)

library_path = os.path.dirname(os.path.abspath(dbfile))
(library_path, _) = os.path.split(library_path) # drop /database from path
if not library_path:
# library_path not provided as argument (this is the normal case)
# determine library path relative to the database path
library_path = os.path.dirname(os.path.abspath(dbfile))
(library_path, _) = os.path.split(library_path) # drop /database from path
self._library_path = library_path
if int(self._db_version) <= int(_PHOTOS_4_VERSION):
masters_path = os.path.join(library_path, "Masters")
Expand Down
23 changes: 23 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1615,6 +1615,29 @@ def test_export_alt_copy():
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)


def test_export_alt_db():
"""test export with --alt-db"""
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
cwd_temp = os.getcwd()
database_temp = os.path.join(cwd_temp, "database")
shutil.copytree(
os.path.join(cwd, CLI_PHOTOS_DB, "database"),
database_temp,
)
library_root = os.path.join(cwd, CLI_PHOTOS_DB)
alt_db = os.path.join(database_temp, "Photos.sqlite")
result = runner.invoke(
export,
["--library", library_root, "--alt-db", alt_db, ".", "-V"],
)
assert result.exit_code == 0
files = glob.glob("*")
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES + ["database"])


def test_export_tmpdir():
"""test basic export with --tmpdir"""
runner = CliRunner()
Expand Down
24 changes: 22 additions & 2 deletions tests/test_ventura_13_0_0.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Test macOS 13.0 Photos library"""

import json
import os
import shutil
import tempfile
from collections import namedtuple

import pytest
Expand All @@ -9,8 +12,8 @@
from osxphotos._constants import _UNKNOWN_PERSON

PHOTOS_DB = "tests/Test-13.0.0.photoslibrary/database/photos.db"
PHOTOS_DB_PATH = "/Test-13.0.0.photoslibrary/database/photos.db"
PHOTOS_LIBRARY_PATH = "/Test-13.0.0.photoslibrary"
PHOTOS_DB_PATH = "tests/Test-13.0.0.photoslibrary/database/photos.db"
PHOTOS_LIBRARY_PATH = "tests/Test-13.0.0.photoslibrary"

PHOTOS_DB_LEN = 16
PHOTOS_NOT_IN_TRASH_LEN = 14
Expand Down Expand Up @@ -1373,3 +1376,20 @@ def test_tables(photosdb: osxphotos.PhotosDB):
assert tables.ZADDITIONALASSETATTRIBUTES.ZTITLE[0] == photo.title
assert len(tables.ZASSET.rows()) == 1
assert tables.ZASSET.rows_dict()[0]["ZUUID"] == photo.uuid


def test_photosdb_library_path():
"""Test the library_path argument to PhotosDB"""

# copy the /database directory of PHOTOS_LIBRARY_PATH to a temp dir
# and use that as the dbpath
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
os.path.join(PHOTOS_LIBRARY_PATH, "database"),
os.path.join(tmpdir, "database"),
)
dbpath = os.path.join(tmpdir, "database", "Photos.sqlite")
photosdb = osxphotos.PhotosDB(dbpath, library_path=PHOTOS_LIBRARY_PATH)
assert photosdb.library_path == PHOTOS_LIBRARY_PATH
assert photosdb.db_path == dbpath
assert len(photosdb) == PHOTOS_DB_LEN

0 comments on commit 19a103d

Please sign in to comment.