Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a941f66d62 | ||
|
|
d77eba12b2 | ||
|
|
de94fd76de |
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.41.4](https://github.com/RhetTbull/osxphotos/compare/v0.41.3...v0.41.4)
|
||||
|
||||
> 22 March 2021
|
||||
|
||||
- Bump pillow from 7.2.0 to 8.1.1 [`#399`](https://github.com/RhetTbull/osxphotos/pull/399)
|
||||
- Added --from-time, --to-time, closes #400 [`#400`](https://github.com/RhetTbull/osxphotos/issues/400)
|
||||
|
||||
#### [v0.41.3](https://github.com/RhetTbull/osxphotos/compare/v0.41.2...v0.41.3)
|
||||
|
||||
> 14 March 2021
|
||||
|
||||
13
README.md
13
README.md
@@ -1394,7 +1394,7 @@ Returns a list of the keywords found in the Photos library
|
||||
albums = photosdb.album_info
|
||||
```
|
||||
|
||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing albums in the database or empty list if there are no albums. See also [albums](#albums).
|
||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing albums in the database or empty list if there are no albums. See also [albums](#albums) and [burst_album_info](#burst_album_info).
|
||||
|
||||
#### `albums`
|
||||
```python
|
||||
@@ -1402,7 +1402,7 @@ Returns a list of [AlbumInfo](#AlbumInfo) objects representing albums in the dat
|
||||
album_names = photosdb.albums
|
||||
```
|
||||
|
||||
Returns a list of the album names found in the Photos library.
|
||||
Returns a list of the album names found in the Photos library. See also [burst_albums](#burst_albums).
|
||||
|
||||
**Note**: In Photos 5.0 (MacOS 10.15/Catalina), It is possible to have more than one album with the same name in Photos. Albums with duplicate names are treated as a single album and the photos in each are combined. For example, if you have two albums named "Wedding" and each has 2 photos, osxphotos will treat this as a single album named "Wedding" with 4 photos in it.
|
||||
|
||||
@@ -1838,6 +1838,9 @@ Returns Uniform Type Identifier (UTI) for the associated raw image, if there is
|
||||
Returns True if photos is a burst image (e.g. part of a set of burst images), otherwise False.
|
||||
See [burst_photos](#burst_photos)
|
||||
|
||||
#### `burst_selected`
|
||||
Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False.
|
||||
|
||||
#### `burst_photos`
|
||||
If photo is a burst image (see [burst](#burst)), returns a list of PhotoInfo objects for all other photos in the same burst set. If not a burst image, returns empty list.
|
||||
|
||||
@@ -1861,6 +1864,12 @@ IMG_9854.JPG
|
||||
IMG_9855.JPG
|
||||
```
|
||||
|
||||
#### `burst_albums`
|
||||
If photo is a non-selected burst photo, returns a list of albums any other photos in the same burst set, are contained in. Otherwise, returns `PhotoInfo.albums`. If a burst photo which has unselected burst images (e.g. the burst images are in the library but haven't been selected by the user using the "Make a selection" feature) is placed in a an album, Photos treats only the selected "key" photo as in the album. The unselected burst images, while associated with the photo in the album, are not technically in the album. If you are handling one of these unselected burst photos and want to know which album it would be in based on which albums it's selected key images are in, use `burst_albums`. See also [burst_album_info](#burst_album_info) and [albums](#albums).
|
||||
|
||||
#### `burst_album_info`
|
||||
If photo is non-selected burst photo, teturns a list of [AlbumInfo](#AlbumInfo) objects representing the albums any other photos in the same burst set are contained in. Otherwise, returns `PhotoInfo.album_info`. See also [burst_albums](#burst_albums) and [album_info](#album_info).
|
||||
|
||||
#### `live_photo`
|
||||
Returns True if photo is an Apple live photo (ie. it has an associated "live" video component), otherwise returns False. See [path_live_photo](#path_live_photo).
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.41.4"
|
||||
__version__ = "0.41.5"
|
||||
|
||||
@@ -1408,6 +1408,9 @@ def export(
|
||||
is_reference=is_reference,
|
||||
in_album=in_album,
|
||||
not_in_album=not_in_album,
|
||||
burst_photos=export_bursts,
|
||||
# skip missing bursts if using --download-missing by itself as AppleScript otherwise causes errors
|
||||
missing_bursts=(download_missing and use_photokit) or not download_missing,
|
||||
)
|
||||
|
||||
if photos:
|
||||
@@ -1416,13 +1419,6 @@ def export(
|
||||
previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()}
|
||||
photos = [p for p in photos if p.uuid not in previous_uuids]
|
||||
|
||||
if export_bursts:
|
||||
# add the burst_photos to the export set
|
||||
photos_burst = [p for p in photos if p.burst]
|
||||
for burst in photos_burst:
|
||||
burst_set = [p for p in burst.burst_photos if not p.ismissing]
|
||||
photos.extend(burst_set)
|
||||
|
||||
num_photos = len(photos)
|
||||
# TODO: photos or photo appears several times, pull into a separate function
|
||||
photo_str = "photos" if num_photos > 1 else "photo"
|
||||
@@ -2019,6 +2015,8 @@ def _query(
|
||||
is_reference=False,
|
||||
in_album=False,
|
||||
not_in_album=False,
|
||||
burst_photos=None,
|
||||
missing_bursts=None,
|
||||
):
|
||||
"""Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands
|
||||
|
||||
@@ -2262,6 +2260,27 @@ def _query(
|
||||
if to_time:
|
||||
photos = [p for p in photos if p.date.time() <= to_time]
|
||||
|
||||
if burst_photos:
|
||||
# add the burst_photos to the export set
|
||||
photos_burst = [p for p in photos if p.burst]
|
||||
for burst in photos_burst:
|
||||
if missing_bursts:
|
||||
# include burst photos that are missing
|
||||
photos.extend(burst.burst_photos)
|
||||
else:
|
||||
# don't include missing burst images (these can't be downloaded with AppleScript)
|
||||
photos.extend([p for p in burst.burst_photos if not p.ismissing])
|
||||
|
||||
# remove duplicates as each burst photo in the set that's selected would
|
||||
# result in the entire set being added above
|
||||
# can't use set() because PhotoInfo not hashable
|
||||
seen_uuids = {}
|
||||
for p in photos:
|
||||
if p.uuid in seen_uuids:
|
||||
continue
|
||||
seen_uuids[p.uuid] = p
|
||||
photos = list(seen_uuids.values())
|
||||
|
||||
return photos
|
||||
|
||||
|
||||
|
||||
@@ -453,9 +453,24 @@ class PhotoInfo:
|
||||
)
|
||||
return self._albums
|
||||
|
||||
@property
|
||||
def burst_albums(self):
|
||||
"""If photo is non-selected burst photo, list of albums any other images in the same burst set are contained in, otherwise returns self.albums"""
|
||||
if self.burst_selected or not self.burst:
|
||||
return self.albums
|
||||
|
||||
try:
|
||||
return self._burst_albums
|
||||
except AttributeError:
|
||||
burst_albums = []
|
||||
for photo in self.burst_photos:
|
||||
burst_albums.extend(photo.albums)
|
||||
self._burst_albums = list(set(burst_albums))
|
||||
return self._burst_albums
|
||||
|
||||
@property
|
||||
def album_info(self):
|
||||
""" list of AlbumInfo objects representing albums the photos is contained in """
|
||||
""" list of AlbumInfo objects representing albums the photo is contained in """
|
||||
try:
|
||||
return self._album_info
|
||||
except AttributeError:
|
||||
@@ -465,6 +480,21 @@ class PhotoInfo:
|
||||
]
|
||||
return self._album_info
|
||||
|
||||
@property
|
||||
def burst_album_info(self):
|
||||
""" If photo is a non-selected burst photo, returns list of AlbumInfo objects representing albums any other photos in the same burst set are contained in, otherwise returns self.album_info """
|
||||
if self.burst_selected or not self.burst:
|
||||
return self.album_info
|
||||
|
||||
try:
|
||||
return self._burst_album_info
|
||||
except AttributeError:
|
||||
burst_album_info = []
|
||||
for photo in self.burst_photos:
|
||||
burst_album_info.extend(photo.album_info)
|
||||
self._burst_album_info = list(set(burst_album_info))
|
||||
return self._burst_album_info
|
||||
|
||||
@property
|
||||
def import_info(self):
|
||||
""" ImportInfo object representing import session for the photo or None if no import session """
|
||||
@@ -680,6 +710,11 @@ class PhotoInfo:
|
||||
""" Returns True if photo is part of a Burst photo set, otherwise False """
|
||||
return self._info["burst"]
|
||||
|
||||
@property
|
||||
def burst_selected(self):
|
||||
""" Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False """
|
||||
return self._info["burst_key"]
|
||||
|
||||
@property
|
||||
def burst_photos(self):
|
||||
"""If photo is a burst photo, returns list of PhotoInfo objects
|
||||
|
||||
@@ -840,7 +840,7 @@ class PhotoTemplate:
|
||||
""" return list of values for a multi-valued template field """
|
||||
values = []
|
||||
if field == "album":
|
||||
values = self.photo.albums
|
||||
values = self.photo.burst_albums if self.photo.burst else self.photo.albums
|
||||
elif field == "keyword":
|
||||
values = self.photo.keywords
|
||||
elif field == "person":
|
||||
@@ -854,7 +854,11 @@ class PhotoTemplate:
|
||||
elif field == "folder_album":
|
||||
values = []
|
||||
# photos must be in an album to be in a folder
|
||||
for album in self.photo.album_info:
|
||||
if self.photo.burst:
|
||||
album_info = self.photo.burst_album_info
|
||||
else:
|
||||
album_info = self.photo.album_info
|
||||
for album in album_info:
|
||||
if album.folder_names:
|
||||
# album in folder
|
||||
if dirname:
|
||||
|
||||
@@ -181,7 +181,7 @@ pyobjc-framework-Vision==6.2.2
|
||||
pyobjc-framework-WebKit==6.2.2
|
||||
pyparsing==2.4.1.1
|
||||
python-dateutil==2.8.1
|
||||
PyYAML==5.1.2
|
||||
PyYAML==5.4
|
||||
pyzmq==18.1.1
|
||||
readme-renderer==25.0
|
||||
regex==2020.2.20
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -22,6 +22,17 @@ PHOTOS_DB_TOUCH = PHOTOS_DB_15_6
|
||||
PHOTOS_DB_14_6 = "tests/Test-10.14.6.photoslibrary"
|
||||
PHOTOS_DB_MOVIES = "tests/Test-Movie-5_0.photoslibrary"
|
||||
|
||||
# my personal library which some tests require
|
||||
PHOTOS_DB_RHET = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
UUID_BURST_ALBUM = "9F90DC00-AAAF-4A05-9A65-61FEEE0D67F2" # in my personal library
|
||||
BURST_ALBUM_FILES = [
|
||||
"IMG_9812.JPG",
|
||||
"IMG_9813.JPG",
|
||||
"IMG_9814.JPG",
|
||||
"IMG_9815.JPG",
|
||||
"IMG_9816.JPG",
|
||||
]
|
||||
|
||||
UUID_FILE = "tests/uuid_from_file.txt"
|
||||
|
||||
CLI_OUTPUT_NO_SUBCOMMAND = [
|
||||
@@ -5649,3 +5660,42 @@ def test_export_jpeg_ext_convert_to_jpeg_movie():
|
||||
assert f"{filename}.jpg".lower() not in files
|
||||
assert f"{filename}.{ext}".lower() in files
|
||||
assert f"{filename}_edited.{ext}".lower() in files
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"OSXPHOTOS_TEST_EXPORT" not in os.environ,
|
||||
reason="Skip if not running on author's personal library.",
|
||||
)
|
||||
def test_export_burst_folder_album():
|
||||
""" test non-selected burst photos are exported with the album their key photo is in, issue #401 """
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
from osxphotos.cli import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_RHET),
|
||||
".",
|
||||
"-V",
|
||||
"--directory",
|
||||
"{folder_album}",
|
||||
"--uuid",
|
||||
UUID_BURST_ALBUM,
|
||||
"--download-missing",
|
||||
"--use-photokit",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
folder_album = pathlib.Path("TestBurst")
|
||||
assert folder_album.is_dir()
|
||||
for filename in BURST_ALBUM_FILES:
|
||||
path = folder_album / filename
|
||||
assert path.is_file()
|
||||
|
||||
|
||||
@@ -17,6 +17,34 @@ UUID_DICT = {
|
||||
"live": "BFF29EBD-22DF-4FCF-9817-317E7104EA50",
|
||||
}
|
||||
|
||||
UUID_BURSTS = {
|
||||
"9F90DC00-AAAF-4A05-9A65-61FEEE0D67F2": {
|
||||
"selected": True,
|
||||
"filename": "IMAGE_9812.JPG",
|
||||
"albums": ["TestBurst"],
|
||||
},
|
||||
"964F457D-5FFC-47B9-BEAD-56B0A83FEF63": {
|
||||
"selected": True,
|
||||
"filename": "IMG_9816.JPG",
|
||||
"albums": [],
|
||||
},
|
||||
"A385FA13-DF8E-482F-A8C5-970EDDF54C2F": {
|
||||
"selected": False,
|
||||
"filename": "IMG_9813.JPG",
|
||||
"albums": ["TestBurst", "TestBurst2"],
|
||||
},
|
||||
"38F8F30C-FF6D-49DA-8092-18497F1D6628": {
|
||||
"selected": True,
|
||||
"filename": "IMG_9814.JPG",
|
||||
"albums": ["TestBurst2"],
|
||||
},
|
||||
"E3863443-9EA8-417F-A90B-8F7086623DAD": {
|
||||
"selected": False,
|
||||
"filename": "IMG_9815.JPG",
|
||||
"albums": ["TestBurst", "TestBurst2"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def photosdb():
|
||||
@@ -156,3 +184,13 @@ def test_export_edited_no_edit(photosdb):
|
||||
with pytest.raises(Exception) as e:
|
||||
assert photos[0].export(dest, use_photos_export=True, edited=True)
|
||||
assert e.type == ValueError
|
||||
|
||||
|
||||
def test_burst_albums(photosdb):
|
||||
"""Test burst_selected, burst_albums"""
|
||||
|
||||
for uuid in UUID_BURSTS:
|
||||
photo = photosdb.get_photo(uuid)
|
||||
assert photo.burst
|
||||
assert photo.burst_selected == UUID_BURSTS[uuid]["selected"]
|
||||
assert sorted(photo.burst_albums) == sorted(UUID_BURSTS[uuid]["albums"])
|
||||
|
||||
Reference in New Issue
Block a user