Skip to content

Reference

PhotosLibrary

Source code in photoscript/__init__.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
class PhotosLibrary:
    def __init__(self):
        """create new PhotosLibrary object and launch Photos"""
        run_script("photosLibraryWaitForPhotos", 300)
        self._version = str(run_script("photosLibraryVersion"))

    def activate(self):
        """activate Photos.app"""
        run_script("photosLibraryActivate")

    def quit(self):
        """quit Photos.app"""
        run_script("photosLibraryQuit")

    def open(self, library_path, delay=10):
        """open a library and wait for delay for user to acknowledge in Photos"""
        # Note: Unlike the other AppleScript scripts, this one is not included in photoscript.applescript
        # because, for reasons I cannot explain, it fails to run if included there
        if not pathlib.Path(library_path).is_dir():
            raise ValueError(f"{library_path} does not appear to be a Photos library")
        self.activate()
        script = AppleScript(
            f"""
            set tries to 0
            repeat while tries < 5
                try
                    tell application "Photos"
                        activate
                        delay 3 
                        open POSIX file "{library_path}"
                        delay {delay}
                    end tell
                    set tries to 5
                on error
                    set tries to tries + 1
                end try
            end repeat
        """
        )
        script.run()

    @property
    def running(self):
        """True if Photos is running, otherwise False"""
        return run_script("photosLibraryIsRunning")

    def hide(self):
        """Tell Photos to hide its window"""
        run_script("photosLibraryHide")

    @property
    def hidden(self):
        """True if Photos is hidden (or not running), False if Photos is visible"""
        return run_script("photosLibraryIsHidden")

    @property
    def name(self):
        """name of Photos.app"""
        return run_script("photosLibraryName")

    @property
    def version(self):
        """version of Photos.app as str"""
        return self._version

    @property
    def frontmost(self):
        """True if Photos.app is front most app otherwise False"""
        return run_script("photosLibraryIsFrontMost")

    @property
    def selection(self):
        """List of Photo objects for currently selected photos or [] if no selection"""
        uuids = run_script("photosLibraryGetSelection")
        return [Photo(uuid) for uuid in uuids]

    @property
    def favorites(self):
        """Album object for the Favorites album"""
        fav_id = run_script("photosLibraryFavorites")
        return Album(fav_id)

    # doesn't seem to be a way to do anything with the recently deleted album except count items
    # @property
    # def recently_deleted(self):
    #     """ Album object for the Recently Deleted album """
    #     del_id = run_script("photosLibraryRecentlyDeleted")
    #     return Album(del_id)

    def photos(self, search=None, uuid=None, range_=None):
        """Returns a generator that yields Photo objects for media items in the library.

        Args:
            search: optional text string to search for (returns matching items)
            uuid: optional list of UUIDs to get
            range: optional list of [start, stop] sequence of photos to get

        Returns:
            Generator that yields Photo objects

        Raises:
            ValueError if more than one of search, uuid, range passed or invalid range
            TypeError if list not passed for range

        Note: photos() returns a generator instead of a list because retrieving all the photos
        from a large Photos library can take a very long time--on my system, the rate is about 1
        per second; this is limited by the Photos AppleScript interface and I've not found
        anyway to speed it up.  Using a generator allows you process photos individually rather
        than waiting, possibly hours, for Photos to return the results.

        range works like python's range function.  Thus range=[0,4] will return
        Photos 0, 1, 2, 3; range=[10] returns the first 10 photos in the library;
        range start must be in range 0 to len(PhotosLibrary())-1,
        stop in range 1 to len(PhotosLibrary()).  You may be able to optimize the speed by which
        photos are return by chunking up requests in batches of photos using range,
        e.g. request 10 photos at a time.
        """
        if len([x for x in [search, uuid, range_] if x]) > 1:
            raise ValueError("Cannot pass more than one of search, uuid, range_")

        if not any([search, uuid, range_]):
            return self._iterphotos()

        if search is not None:
            # search for text
            photo_ids = run_script("photosLibrarySearchPhotos", search)
        elif uuid:
            # search by uuid
            photo_ids = uuid
        else:
            # search by range
            if not isinstance(range_, list):
                raise TypeError("range_ must be a list")

            if not (1 <= len(range_) <= 2):
                raise ValueError("invalid range, must be list of len 1 or 2")

            if len(range_) == 1:
                start = 0
                stop = range_[0]
            else:
                start, stop = range_

            if start > stop:
                raise ValueError("start range must be <= stop range")

            count = len(self)
            if not ((0 <= start <= count - 1) and (1 <= stop <= count)):
                raise ValueError(
                    f"invalid range: valid range is start: 0 to {count-1}, stop: 1 to {count}"
                )

            photo_ids = run_script("photosLibraryGetPhotoByRange", start + 1, stop)

        return self._iterphotos(uuids=photo_ids) if photo_ids else []

    def _iterphotos(self, uuids=None):
        if uuids:
            for uuid in uuids:
                yield Photo(uuid)
        else:
            # return all photos via generator
            count = len(self)
            for x in range(1, count + 1):
                # AppleScript list indexes start at 1
                photo_id = run_script("photosLibraryGetPhotoByRange", x, x)[0]
                yield Photo(photo_id)

    def import_photos(self, photo_paths, album=None, skip_duplicate_check=False):
        """import photos

        Args:
            photo_paths: list of file paths to import as str or pathlib.Path
            album: optional, Album object for album to import into
            skip_duplicate_check: if True, Photos will not check for duplicates on import, default is False.

        Returns:
            list of Photo objects for imported photos

        NOTE: If you attempt to import a duplicate photo and skip_duplicate_check != True,
            Photos will block with drop-down sheet until the user clicks "Cancel, Import, or Don't Import."
        """
        # stringify paths in case pathlib.Path paths are passed
        photo_paths = [str(photo_path) for photo_path in photo_paths]
        if album is not None:
            photo_ids = run_script(
                "photosLibraryImportToAlbum",
                photo_paths,
                album.id,
                skip_duplicate_check,
            )
        else:
            photo_ids = run_script(
                "photosLibraryImport", photo_paths, skip_duplicate_check
            )

        return [Photo(photo) for photo in photo_ids]

    def album_names(self, top_level=False):
        """List of album names in the Photos library

        Args:
            top_level: if True, returns only top-level albums otherwise also returns albums in sub-folders; default is False
        """
        return run_script("photosLibraryAlbumNames", top_level)

    def folder_names(self, top_level=False):
        """List of folder names in the Photos library

        Args:
            top_level: if True, returns only top-level folders otherwise also returns sub-folders; default is False
        """
        return run_script("photosLibraryFolderNames", top_level)

    def album(self, *name, uuid=None, top_level=False):
        """Album instance by name or id

        Args:
            name: name of album
            uuid: id of album
            top_level: if True, searches only top level albums; default = False

        Returns:
            Album object or None if album could not be found

        Raises:
            ValueError if both name and id passed or neither passed.

        Must pass only name or id but not both.
        If more than one album with same name, returns the first one found.
        """
        if (not name and uuid is None) or (name and uuid is not None):
            raise ValueError("Must pass only name or uuid but not both")

        if name:
            uuid = run_script("albumByName", name[0], top_level)
            if uuid != 0:
                return Album(uuid)
            else:
                return None
        else:
            return Album(uuid)

    def albums(self, top_level=False):
        """list of Album objects for all albums"""
        album_ids = run_script("photosLibraryAlbumIDs", top_level)
        return [Album(uuid) for uuid in album_ids]

    def create_album(self, name, folder: "Folder" = None) -> "Album":
        """creates an album

        Args:
            name: name of new album
            folder: Folder object in which to create new album.
                    If None, creates top-level album.  Default is None.

        Returns:
            Album object for newly created album

        Raises:
            AppleScriptError if error creating the album
        """
        if folder is None:
            album_id = run_script("photosLibraryCreateAlbum", name)
        else:
            album_id = run_script(
                "photosLibraryCreateAlbumAtFolder", name, folder.idstring
            )

        if album_id != kMissingValue:
            return Album(album_id)
        else:
            raise AppleScriptError(f"Could not create album {name}")

    def delete_album(self, album: "Album"):
        """deletes album (but does not delete photos in the album)

        Args:
            album: an Album object for album to delete
        """
        return run_script("photosLibraryDeleteAlbum", album.id)

    def folder(
        self, name: str = None, path: list[str] = None, uuid: str = None, top_level=True
    ):
        """Folder instance by name or uuid

        Args:
            name: name of folder, e.g. "My Folder"
            path: path of folder as list of strings, e.g. ["My Folder", "Subfolder"]
            uuid: id of folder, e.g. "F1234567-1234-1234-1234-1234567890AB"
            top_level: if True, only searches top level folders by name; default is True

        Returns:
            Folder object or None if folder could not be found

        Raises:
            ValueError not one of name, path, or uuid is passed

        Notes:
            Must pass one of path, name, or uuid but not more than one
            If more than one folder with same name, returns first one found.
        """
        if sum(bool(x) for x in [name, path, uuid]) != 1:
            raise ValueError(
                "Must pass one of name, path, or uuid but not more than one"
            )

        if path:
            idstring = run_script("folderGetIDStringFromPath", path)
            return Folder(idstring=idstring) if idstring != kMissingValue else None

        if name:
            idstring = run_script(
                "photosLibraryGetFolderIDStringForName", name, top_level
            )
            return Folder(idstring=idstring) if idstring != kMissingValue else None

        if uuid:
            idstring = run_script(
                "photosLibraryGetFolderIDStringForID", uuid, top_level
            )
            return Folder(idstring=idstring) if idstring != kMissingValue else None

    def folder_by_path(self, folder_path):
        """Return folder in the library by path

        Args:
            folder_path: list of folder names in descending path order, e.g. ["Folder", "SubFolder1", "SubFolder2"]

        Returns:
            Folder object for folder at folder_path or None if not found
        """
        folder_id = run_script("folderIDByPath", folder_path)
        return Folder(folder_id) if folder_id != kMissingValue else None

    def folders(self, top_level=True):
        """list of Folder objects for all folders"""
        folder_ids = run_script("photosLibraryFolderIDs", top_level)
        return [Folder(uuid) for uuid in folder_ids]

    def create_folder(self, name: str, folder: "Folder" = None) -> "Folder":
        """creates a folder

        Args:
            name: name of new folder
            folder: Folder object in which to create the new folder.
                    If None, creates top-level folder. Default is None.

        Returns:
            Folder object for newly created folder

        Raises:
            AppleScriptError if folder cannot be created
        """
        if folder is None:
            folder_id = run_script("photosLibraryCreateFolder", name)
        else:
            folder_id = run_script(
                "photosLibraryCreateFolderAtFolder", name, folder.idstring
            )

        if folder_id != kMissingValue:
            return Folder(idstring=folder_id)
        else:
            raise AppleScriptError(f"Could not create folder {name}")

    def make_folders(self, folder_path):
        """Recursively makes folders and subfolders.  Works similar to os.makedirs_.
        If any component of folder_path already exists, does not raise error.

        .. _os.makedirs: https://docs.python.org/3/library/os.html#os.makedirs

        Args:
            folder_path: list of folder names in descending path order, e.g. ["Folder", "SubFolder1", "SubFolder2"]

        Returns:
            Folder object for the final sub folder

        Raises:
            ValueError if folder_path is empty
            TypeError if folder_path is not a list
        """
        if not isinstance(folder_path, list):
            raise TypeError("list expected for folder_path")
        if not folder_path:
            raise ValueError("no values in folder_path")

        folder = self.folder(folder_path[0], top_level=True)
        if folder is None:
            folder = self.create_folder(folder_path[0])
        for subfolder_name in folder_path[1:]:
            subfolder = folder.folder(subfolder_name)
            if subfolder is None:
                subfolder = folder.create_folder(subfolder_name)
            folder = subfolder
        return folder

    def make_album_folders(self, album_name, folder_path):
        """Make album in a folder path.  If either the album or any component of the
           folder path doesn't exist, it will be created.  If album or folder path
           does exist, no duplicate is created.  Folder path is created recursively
           if needed.

        Args:
            album_name: name of album to create.  If album already exists, returns existing album.
            folder_path: list of folder names in descending path order, e.g. ["Folder", "SubFolder1", "SubFolder2"].

        Returns:
            Album object.

        Raises:
            ValueError if folder_path is empty or album_name is None.
            TypeError if folder_path is not a list.
        """
        if album_name is None or not len(album_name):
            raise ValueError("album_name must not be None")
        if not isinstance(folder_path, list):
            raise TypeError("list expected for folder_path")
        if not folder_path:
            raise ValueError("no values in folder_path")

        folder = self.make_folders(folder_path)
        album = folder.album(album_name)
        if album is None:
            album = folder.create_album(album_name)
        return album

    def delete_folder(self, folder: "Folder"):
        """Deletes folder (and all its sub-folders and albums)

        Args:
            folder: a Folder object for folder to delete

        Notes:
            On macOS 10.15 & above, only top-level folders can be deleted.
            Sub-folders cannot be deleted due to a bug in Photos' AppleScript
            implementation.
        """
        return run_script("photosLibraryDeleteFolder", folder.idstring)

    def __len__(self):
        return run_script("photosLibraryCount")

    # TODO: add a temp_album() method that creates a temporary album
    def _temp_album_name(self):
        """get a temporary album name that doesn't clash with album in the library"""
        temp_name = self._temp_name()
        while self.album(temp_name) is not None:
            temp_name = self._temp_name()
        return temp_name

    def _temp_name(self):
        ds = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
        random_str = "".join(
            random.choice(string.ascii_lowercase + string.ascii_uppercase)
            for i in range(10)
        )
        return f"photoscript_{ds}_{random_str}"

    def _export_photo(
        self,
        photo,
        export_path,
        original=False,
        overwrite=False,
        timeout=120,
        reveal_in_finder=False,
    ):
        """Export photo to export_path

        Args:
            photo: Photo object to export
            export_path: path to export to
            original: if True, export original image, otherwise export current image; default = False
            overwrite: if True, export will overwrite a file of same name as photo in export_path; default = False
            timeout: number of seconds to wait for Photos to complete export before timing out; default = 120
            reveal_in_finder: if True, will open Finder with exported items selected when done; default = False

        Returns:
            List of full paths of exported photos.  There may be more than one photo exported due
            to live images and burst images.

        Raises:
            ValueError if export_path is not a valid directory

        Note: Photos always exports as high-quality JPEG unless original=True.
        If original=True, will export all burst images for burst photos and
        live movie for live photos.  If original=False, only the primary image from a
        burst set will be exported for burst photos and the live movie component of a
        live image will not be exported, only the JPEG component.
        """

        dest = pathlib.Path(export_path)
        if not dest.is_dir():
            raise ValueError(f"export_path {export_path} must be a directory")

        edited = not original

        tmpdir = tempfile.TemporaryDirectory(prefix="photoscript_")

        # export original
        filename = run_script(
            "photoExport", photo.id, tmpdir.name, original, edited, timeout
        )

        exported_paths = []
        if filename is not None:
            # need to find actual filename as sometimes Photos renames JPG to jpeg on export
            # may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)
            # TemporaryDirectory will cleanup on return
            files = glob.glob(os.path.join(tmpdir.name, "*"))
            seen_files = {}
            for fname in files:
                path = pathlib.Path(fname)
                dest_new = dest / path.name
                if not overwrite:
                    # ensure there are no name collisions on export
                    try:
                        dest_update = seen_files[path.stem]
                    except KeyError:
                        count = 1
                        dest_files = findfiles(
                            f"{dest_new.stem}*", str(dest_new.parent)
                        )
                        dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
                        dest_update = dest_new.stem
                        while dest_update.lower() in dest_files:
                            dest_update = f"{dest_new.stem} ({count})"
                            count += 1
                        seen_files[path.stem] = dest_update
                    dest_new = dest_new.parent / f"{dest_update}{dest_new.suffix}"
                ditto(str(path), str(dest_new))
                exported_paths.append(str(dest_new))
            if reveal_in_finder:
                run_script("revealInFinder", exported_paths)
        return exported_paths

favorites property

Album object for the Favorites album

frontmost property

True if Photos.app is front most app otherwise False

hidden property

True if Photos is hidden (or not running), False if Photos is visible

name property

name of Photos.app

running property

True if Photos is running, otherwise False

selection property

List of Photo objects for currently selected photos or [] if no selection

version property

version of Photos.app as str

__init__()

create new PhotosLibrary object and launch Photos

Source code in photoscript/__init__.py
60
61
62
63
def __init__(self):
    """create new PhotosLibrary object and launch Photos"""
    run_script("photosLibraryWaitForPhotos", 300)
    self._version = str(run_script("photosLibraryVersion"))

activate()

activate Photos.app

Source code in photoscript/__init__.py
65
66
67
def activate(self):
    """activate Photos.app"""
    run_script("photosLibraryActivate")

album(*name, uuid=None, top_level=False)

Album instance by name or id

Parameters:

Name Type Description Default
name

name of album

()
uuid

id of album

None
top_level

if True, searches only top level albums; default = False

False

Returns:

Type Description

Album object or None if album could not be found

Must pass only name or id but not both. If more than one album with same name, returns the first one found.

Source code in photoscript/__init__.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
def album(self, *name, uuid=None, top_level=False):
    """Album instance by name or id

    Args:
        name: name of album
        uuid: id of album
        top_level: if True, searches only top level albums; default = False

    Returns:
        Album object or None if album could not be found

    Raises:
        ValueError if both name and id passed or neither passed.

    Must pass only name or id but not both.
    If more than one album with same name, returns the first one found.
    """
    if (not name and uuid is None) or (name and uuid is not None):
        raise ValueError("Must pass only name or uuid but not both")

    if name:
        uuid = run_script("albumByName", name[0], top_level)
        if uuid != 0:
            return Album(uuid)
        else:
            return None
    else:
        return Album(uuid)

album_names(top_level=False)

List of album names in the Photos library

Parameters:

Name Type Description Default
top_level

if True, returns only top-level albums otherwise also returns albums in sub-folders; default is False

False
Source code in photoscript/__init__.py
257
258
259
260
261
262
263
def album_names(self, top_level=False):
    """List of album names in the Photos library

    Args:
        top_level: if True, returns only top-level albums otherwise also returns albums in sub-folders; default is False
    """
    return run_script("photosLibraryAlbumNames", top_level)

albums(top_level=False)

list of Album objects for all albums

Source code in photoscript/__init__.py
302
303
304
305
def albums(self, top_level=False):
    """list of Album objects for all albums"""
    album_ids = run_script("photosLibraryAlbumIDs", top_level)
    return [Album(uuid) for uuid in album_ids]

create_album(name, folder=None)

creates an album

Parameters:

Name Type Description Default
name

name of new album

required
folder 'Folder'

Folder object in which to create new album. If None, creates top-level album. Default is None.

None

Returns:

Type Description
'Album'

Album object for newly created album

Source code in photoscript/__init__.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
def create_album(self, name, folder: "Folder" = None) -> "Album":
    """creates an album

    Args:
        name: name of new album
        folder: Folder object in which to create new album.
                If None, creates top-level album.  Default is None.

    Returns:
        Album object for newly created album

    Raises:
        AppleScriptError if error creating the album
    """
    if folder is None:
        album_id = run_script("photosLibraryCreateAlbum", name)
    else:
        album_id = run_script(
            "photosLibraryCreateAlbumAtFolder", name, folder.idstring
        )

    if album_id != kMissingValue:
        return Album(album_id)
    else:
        raise AppleScriptError(f"Could not create album {name}")

create_folder(name, folder=None)

creates a folder

Parameters:

Name Type Description Default
name str

name of new folder

required
folder 'Folder'

Folder object in which to create the new folder. If None, creates top-level folder. Default is None.

None

Returns:

Type Description
'Folder'

Folder object for newly created folder

Source code in photoscript/__init__.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
def create_folder(self, name: str, folder: "Folder" = None) -> "Folder":
    """creates a folder

    Args:
        name: name of new folder
        folder: Folder object in which to create the new folder.
                If None, creates top-level folder. Default is None.

    Returns:
        Folder object for newly created folder

    Raises:
        AppleScriptError if folder cannot be created
    """
    if folder is None:
        folder_id = run_script("photosLibraryCreateFolder", name)
    else:
        folder_id = run_script(
            "photosLibraryCreateFolderAtFolder", name, folder.idstring
        )

    if folder_id != kMissingValue:
        return Folder(idstring=folder_id)
    else:
        raise AppleScriptError(f"Could not create folder {name}")

delete_album(album)

deletes album (but does not delete photos in the album)

Parameters:

Name Type Description Default
album 'Album'

an Album object for album to delete

required
Source code in photoscript/__init__.py
333
334
335
336
337
338
339
def delete_album(self, album: "Album"):
    """deletes album (but does not delete photos in the album)

    Args:
        album: an Album object for album to delete
    """
    return run_script("photosLibraryDeleteAlbum", album.id)

delete_folder(folder)

Deletes folder (and all its sub-folders and albums)

Parameters:

Name Type Description Default
folder 'Folder'

a Folder object for folder to delete

required
Notes

On macOS 10.15 & above, only top-level folders can be deleted. Sub-folders cannot be deleted due to a bug in Photos' AppleScript implementation.

Source code in photoscript/__init__.py
487
488
489
490
491
492
493
494
495
496
497
498
def delete_folder(self, folder: "Folder"):
    """Deletes folder (and all its sub-folders and albums)

    Args:
        folder: a Folder object for folder to delete

    Notes:
        On macOS 10.15 & above, only top-level folders can be deleted.
        Sub-folders cannot be deleted due to a bug in Photos' AppleScript
        implementation.
    """
    return run_script("photosLibraryDeleteFolder", folder.idstring)

folder(name=None, path=None, uuid=None, top_level=True)

Folder instance by name or uuid

Parameters:

Name Type Description Default
name str

name of folder, e.g. "My Folder"

None
path list[str]

path of folder as list of strings, e.g. ["My Folder", "Subfolder"]

None
uuid str

id of folder, e.g. "F1234567-1234-1234-1234-1234567890AB"

None
top_level

if True, only searches top level folders by name; default is True

True

Returns:

Type Description

Folder object or None if folder could not be found

Notes

Must pass one of path, name, or uuid but not more than one If more than one folder with same name, returns first one found.

Source code in photoscript/__init__.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
def folder(
    self, name: str = None, path: list[str] = None, uuid: str = None, top_level=True
):
    """Folder instance by name or uuid

    Args:
        name: name of folder, e.g. "My Folder"
        path: path of folder as list of strings, e.g. ["My Folder", "Subfolder"]
        uuid: id of folder, e.g. "F1234567-1234-1234-1234-1234567890AB"
        top_level: if True, only searches top level folders by name; default is True

    Returns:
        Folder object or None if folder could not be found

    Raises:
        ValueError not one of name, path, or uuid is passed

    Notes:
        Must pass one of path, name, or uuid but not more than one
        If more than one folder with same name, returns first one found.
    """
    if sum(bool(x) for x in [name, path, uuid]) != 1:
        raise ValueError(
            "Must pass one of name, path, or uuid but not more than one"
        )

    if path:
        idstring = run_script("folderGetIDStringFromPath", path)
        return Folder(idstring=idstring) if idstring != kMissingValue else None

    if name:
        idstring = run_script(
            "photosLibraryGetFolderIDStringForName", name, top_level
        )
        return Folder(idstring=idstring) if idstring != kMissingValue else None

    if uuid:
        idstring = run_script(
            "photosLibraryGetFolderIDStringForID", uuid, top_level
        )
        return Folder(idstring=idstring) if idstring != kMissingValue else None

folder_by_path(folder_path)

Return folder in the library by path

Parameters:

Name Type Description Default
folder_path

list of folder names in descending path order, e.g. ["Folder", "SubFolder1", "SubFolder2"]

required

Returns:

Type Description

Folder object for folder at folder_path or None if not found

Source code in photoscript/__init__.py
383
384
385
386
387
388
389
390
391
392
393
def folder_by_path(self, folder_path):
    """Return folder in the library by path

    Args:
        folder_path: list of folder names in descending path order, e.g. ["Folder", "SubFolder1", "SubFolder2"]

    Returns:
        Folder object for folder at folder_path or None if not found
    """
    folder_id = run_script("folderIDByPath", folder_path)
    return Folder(folder_id) if folder_id != kMissingValue else None

folder_names(top_level=False)

List of folder names in the Photos library

Parameters:

Name Type Description Default
top_level

if True, returns only top-level folders otherwise also returns sub-folders; default is False

False
Source code in photoscript/__init__.py
265
266
267
268
269
270
271
def folder_names(self, top_level=False):
    """List of folder names in the Photos library

    Args:
        top_level: if True, returns only top-level folders otherwise also returns sub-folders; default is False
    """
    return run_script("photosLibraryFolderNames", top_level)

folders(top_level=True)

list of Folder objects for all folders

Source code in photoscript/__init__.py
395
396
397
398
def folders(self, top_level=True):
    """list of Folder objects for all folders"""
    folder_ids = run_script("photosLibraryFolderIDs", top_level)
    return [Folder(uuid) for uuid in folder_ids]

hide()

Tell Photos to hide its window

Source code in photoscript/__init__.py
105
106
107
def hide(self):
    """Tell Photos to hide its window"""
    run_script("photosLibraryHide")

import_photos(photo_paths, album=None, skip_duplicate_check=False)

import photos

Parameters:

Name Type Description Default
photo_paths

list of file paths to import as str or pathlib.Path

required
album

optional, Album object for album to import into

None
skip_duplicate_check

if True, Photos will not check for duplicates on import, default is False.

False

Returns:

Type Description

list of Photo objects for imported photos

If you attempt to import a duplicate photo and skip_duplicate_check != True,

Photos will block with drop-down sheet until the user clicks "Cancel, Import, or Don't Import."

Source code in photoscript/__init__.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def import_photos(self, photo_paths, album=None, skip_duplicate_check=False):
    """import photos

    Args:
        photo_paths: list of file paths to import as str or pathlib.Path
        album: optional, Album object for album to import into
        skip_duplicate_check: if True, Photos will not check for duplicates on import, default is False.

    Returns:
        list of Photo objects for imported photos

    NOTE: If you attempt to import a duplicate photo and skip_duplicate_check != True,
        Photos will block with drop-down sheet until the user clicks "Cancel, Import, or Don't Import."
    """
    # stringify paths in case pathlib.Path paths are passed
    photo_paths = [str(photo_path) for photo_path in photo_paths]
    if album is not None:
        photo_ids = run_script(
            "photosLibraryImportToAlbum",
            photo_paths,
            album.id,
            skip_duplicate_check,
        )
    else:
        photo_ids = run_script(
            "photosLibraryImport", photo_paths, skip_duplicate_check
        )

    return [Photo(photo) for photo in photo_ids]

make_album_folders(album_name, folder_path)

Make album in a folder path. If either the album or any component of the folder path doesn't exist, it will be created. If album or folder path does exist, no duplicate is created. Folder path is created recursively if needed.

Parameters:

Name Type Description Default
album_name

name of album to create. If album already exists, returns existing album.

required
folder_path

list of folder names in descending path order, e.g. ["Folder", "SubFolder1", "SubFolder2"].

required

Returns:

Type Description

Album object.

Source code in photoscript/__init__.py
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
def make_album_folders(self, album_name, folder_path):
    """Make album in a folder path.  If either the album or any component of the
       folder path doesn't exist, it will be created.  If album or folder path
       does exist, no duplicate is created.  Folder path is created recursively
       if needed.

    Args:
        album_name: name of album to create.  If album already exists, returns existing album.
        folder_path: list of folder names in descending path order, e.g. ["Folder", "SubFolder1", "SubFolder2"].

    Returns:
        Album object.

    Raises:
        ValueError if folder_path is empty or album_name is None.
        TypeError if folder_path is not a list.
    """
    if album_name is None or not len(album_name):
        raise ValueError("album_name must not be None")
    if not isinstance(folder_path, list):
        raise TypeError("list expected for folder_path")
    if not folder_path:
        raise ValueError("no values in folder_path")

    folder = self.make_folders(folder_path)
    album = folder.album(album_name)
    if album is None:
        album = folder.create_album(album_name)
    return album

make_folders(folder_path)

Recursively makes folders and subfolders. Works similar to os.makedirs_. If any component of folder_path already exists, does not raise error.

.. _os.makedirs: https://docs.python.org/3/library/os.html#os.makedirs

Parameters:

Name Type Description Default
folder_path

list of folder names in descending path order, e.g. ["Folder", "SubFolder1", "SubFolder2"]

required

Returns:

Type Description

Folder object for the final sub folder

Source code in photoscript/__init__.py
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
def make_folders(self, folder_path):
    """Recursively makes folders and subfolders.  Works similar to os.makedirs_.
    If any component of folder_path already exists, does not raise error.

    .. _os.makedirs: https://docs.python.org/3/library/os.html#os.makedirs

    Args:
        folder_path: list of folder names in descending path order, e.g. ["Folder", "SubFolder1", "SubFolder2"]

    Returns:
        Folder object for the final sub folder

    Raises:
        ValueError if folder_path is empty
        TypeError if folder_path is not a list
    """
    if not isinstance(folder_path, list):
        raise TypeError("list expected for folder_path")
    if not folder_path:
        raise ValueError("no values in folder_path")

    folder = self.folder(folder_path[0], top_level=True)
    if folder is None:
        folder = self.create_folder(folder_path[0])
    for subfolder_name in folder_path[1:]:
        subfolder = folder.folder(subfolder_name)
        if subfolder is None:
            subfolder = folder.create_folder(subfolder_name)
        folder = subfolder
    return folder

open(library_path, delay=10)

open a library and wait for delay for user to acknowledge in Photos

Source code in photoscript/__init__.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def open(self, library_path, delay=10):
    """open a library and wait for delay for user to acknowledge in Photos"""
    # Note: Unlike the other AppleScript scripts, this one is not included in photoscript.applescript
    # because, for reasons I cannot explain, it fails to run if included there
    if not pathlib.Path(library_path).is_dir():
        raise ValueError(f"{library_path} does not appear to be a Photos library")
    self.activate()
    script = AppleScript(
        f"""
        set tries to 0
        repeat while tries < 5
            try
                tell application "Photos"
                    activate
                    delay 3 
                    open POSIX file "{library_path}"
                    delay {delay}
                end tell
                set tries to 5
            on error
                set tries to tries + 1
            end try
        end repeat
    """
    )
    script.run()

photos(search=None, uuid=None, range_=None)

Returns a generator that yields Photo objects for media items in the library.

Parameters:

Name Type Description Default
search

optional text string to search for (returns matching items)

None
uuid

optional list of UUIDs to get

None
range

optional list of [start, stop] sequence of photos to get

required

Returns:

Type Description

Generator that yields Photo objects

Note: photos() returns a generator instead of a list because retrieving all the photos from a large Photos library can take a very long time--on my system, the rate is about 1 per second; this is limited by the Photos AppleScript interface and I've not found anyway to speed it up. Using a generator allows you process photos individually rather than waiting, possibly hours, for Photos to return the results.

range works like python's range function. Thus range=[0,4] will return Photos 0, 1, 2, 3; range=[10] returns the first 10 photos in the library; range start must be in range 0 to len(PhotosLibrary())-1, stop in range 1 to len(PhotosLibrary()). You may be able to optimize the speed by which photos are return by chunking up requests in batches of photos using range, e.g. request 10 photos at a time.

Source code in photoscript/__init__.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def photos(self, search=None, uuid=None, range_=None):
    """Returns a generator that yields Photo objects for media items in the library.

    Args:
        search: optional text string to search for (returns matching items)
        uuid: optional list of UUIDs to get
        range: optional list of [start, stop] sequence of photos to get

    Returns:
        Generator that yields Photo objects

    Raises:
        ValueError if more than one of search, uuid, range passed or invalid range
        TypeError if list not passed for range

    Note: photos() returns a generator instead of a list because retrieving all the photos
    from a large Photos library can take a very long time--on my system, the rate is about 1
    per second; this is limited by the Photos AppleScript interface and I've not found
    anyway to speed it up.  Using a generator allows you process photos individually rather
    than waiting, possibly hours, for Photos to return the results.

    range works like python's range function.  Thus range=[0,4] will return
    Photos 0, 1, 2, 3; range=[10] returns the first 10 photos in the library;
    range start must be in range 0 to len(PhotosLibrary())-1,
    stop in range 1 to len(PhotosLibrary()).  You may be able to optimize the speed by which
    photos are return by chunking up requests in batches of photos using range,
    e.g. request 10 photos at a time.
    """
    if len([x for x in [search, uuid, range_] if x]) > 1:
        raise ValueError("Cannot pass more than one of search, uuid, range_")

    if not any([search, uuid, range_]):
        return self._iterphotos()

    if search is not None:
        # search for text
        photo_ids = run_script("photosLibrarySearchPhotos", search)
    elif uuid:
        # search by uuid
        photo_ids = uuid
    else:
        # search by range
        if not isinstance(range_, list):
            raise TypeError("range_ must be a list")

        if not (1 <= len(range_) <= 2):
            raise ValueError("invalid range, must be list of len 1 or 2")

        if len(range_) == 1:
            start = 0
            stop = range_[0]
        else:
            start, stop = range_

        if start > stop:
            raise ValueError("start range must be <= stop range")

        count = len(self)
        if not ((0 <= start <= count - 1) and (1 <= stop <= count)):
            raise ValueError(
                f"invalid range: valid range is start: 0 to {count-1}, stop: 1 to {count}"
            )

        photo_ids = run_script("photosLibraryGetPhotoByRange", start + 1, stop)

    return self._iterphotos(uuids=photo_ids) if photo_ids else []

quit()

quit Photos.app

Source code in photoscript/__init__.py
69
70
71
def quit(self):
    """quit Photos.app"""
    run_script("photosLibraryQuit")

Photo

Source code in photoscript/__init__.py
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
class Photo:
    def __init__(self, uuid):
        # check to see if we need to add UUID suffix
        uuid, id_ = uuid_to_id(uuid, UUID_SUFFIX_PHOTO)
        if valid := run_script("photoExists", uuid):
            self.id = id_
            self._uuid = uuid
        else:
            raise ValueError(f"Invalid photo id: {uuid}")

    @property
    def uuid(self):
        """UUID of Photo"""
        return self._uuid

    @property
    def name(self):
        """name of photo (read/write)"""
        name = run_script("photoName", self.id)
        return name if name not in [kMissingValue, ""] else ""

    @name.setter
    def name(self, name):
        """set name of photo"""
        name = "" if name is None else name
        return run_script("photoSetName", self.id, name)

    @property
    def title(self):
        """title of photo (alias for name)"""
        return self.name

    @title.setter
    def title(self, title):
        """set title of photo (alias for name)"""
        name = "" if title is None else title
        return run_script("photoSetName", self.id, name)

    @property
    def description(self):
        """description of photo"""
        descr = run_script("photoDescription", self.id)
        return descr if descr != kMissingValue else ""

    @description.setter
    def description(self, descr):
        """set description of photo"""
        descr = "" if descr is None else descr
        return run_script("photoSetDescription", self.id, descr)

    @property
    def keywords(self):
        """list of keywords for photo"""
        keywords = run_script("photoKeywords", self.id)
        if not isinstance(keywords, list):
            keywords = [keywords] if keywords != kMissingValue else []
        return keywords

    @keywords.setter
    def keywords(self, keywords):
        """set keywords to list"""
        keywords = [] if keywords is None else keywords
        return run_script("photoSetKeywords", self.id, keywords)

    @property
    def favorite(self):
        """return favorite status (boolean)"""
        return run_script("photoFavorite", self.id)

    @favorite.setter
    def favorite(self, favorite):
        """set favorite status (boolean)"""
        favorite = bool(favorite)
        return run_script("photoSetFavorite", self.id, favorite)

    @property
    def height(self):
        """height of photo in pixels"""
        return run_script("photoHeight", self.id)

    @property
    def width(self):
        """width of photo in pixels"""
        return run_script("photoWidth", self.id)

    @property
    def altitude(self):
        """GPS altitude of photo in meters"""
        altitude = run_script("photoAltitude", self.id)
        return altitude if altitude != kMissingValue else None

    @property
    def location(self):
        """The GPS latitude and longitude, in a tuple of 2 numbers or None.
        Latitude in range -90.0 to 90.0, longitude in range -180.0 to 180.0.
        """
        location = run_script("photoLocation", self.id)
        location[0] = None if location[0] == kMissingValue else location[0]
        location[1] = None if location[1] == kMissingValue else location[1]
        return tuple(location)

    @location.setter
    def location(self, location):
        """Set GPS latitude and longitude, in a tuple of 2 numbers or None.
        Latitude in range -90.0 to 90.0, longitude in range -180.0 to 180.0.
        """

        if not isinstance(location, tuple) and location is not None:
            raise ValueError("location must be a tuple of (latitude, longitude)")

        location = (None, None) if location is None else location

        if location[0] is not None and not -90.0 <= location[0] <= 90.0:
            raise ValueError("latitude must be in range -90.0 to 90.0")

        if location[1] is not None and not -180.0 <= location[1] <= 180.0:
            raise ValueError("longitude must be in range -180.0 to 180.0")

        location = (
            kMissingValue if location[0] is None else location[0],
            kMissingValue if location[1] is None else location[1],
        )

        return run_script("photoSetLocation", self.id, location)

    @property
    def date(self):
        """date of photo as timezone-naive datetime.datetime object"""
        return run_script("photoDate", self.id)

    @date.setter
    def date(self, date):
        """Set date of photo as timezone-naive datetime.datetime object

        Args:
            date: timezone-naive datetime.datetime object
        """
        return run_script("photoSetDate", self.id, date)

    @property
    def filename(self):
        """filename of photo"""
        return run_script("photoFilename", self.id)

    @property
    def albums(self):
        """list of Album objects for albums photo is contained in"""
        albums = run_script("photoAlbums", self.id)
        return [Album(album) for album in albums]

    def export(
        self,
        export_path,
        original=False,
        overwrite=False,
        timeout=120,
        reveal_in_finder=False,
    ):
        """Export photo

        Args:
            photo: Photo object to export
            export_path: path to export to
            original: if True, export original image, otherwise export current image; default = False
            overwrite: if True, export will overwrite a file of same name as photo in export_path; default = False
            timeout: number of seconds to wait for Photos to complete export before timing out; default = 120
            reveal_in_finder: if True, will open Finder with exported items selected when done; default = False

        Returns:
            List of full paths of exported photos.  There may be more than one photo exported due
            to live images and burst images.

        Raises:
            ValueError if export_path is not a valid directory

        Note: Photos always exports as high-quality JPEG unless original=True.
        If original=True, will export all burst images for burst photos and
        live movie for live photos.  If original=False, only the primary image from a
        burst set will be exported for burst photos and the live movie component of a
        live image will not be exported, only the JPEG component.
        """
        return PhotosLibrary()._export_photo(
            self,
            export_path=export_path,
            original=original,
            overwrite=overwrite,
            timeout=timeout,
            reveal_in_finder=reveal_in_finder,
        )

    def duplicate(self):
        """duplicates the photo and returns Photo object for the duplicate"""
        dup_id = run_script("photoDuplicate", self.id)
        return Photo(dup_id)

    def spotlight(self):
        """spotlight the photo in Photos"""
        run_script("photoSpotlight", self.id)

albums property

list of Album objects for albums photo is contained in

altitude property

GPS altitude of photo in meters

date property writable

date of photo as timezone-naive datetime.datetime object

description property writable

description of photo

favorite property writable

return favorite status (boolean)

filename property

filename of photo

height property

height of photo in pixels

keywords property writable

list of keywords for photo

location property writable

The GPS latitude and longitude, in a tuple of 2 numbers or None. Latitude in range -90.0 to 90.0, longitude in range -180.0 to 180.0.

name property writable

name of photo (read/write)

title property writable

title of photo (alias for name)

uuid property

UUID of Photo

width property

width of photo in pixels

duplicate()

duplicates the photo and returns Photo object for the duplicate

Source code in photoscript/__init__.py
1178
1179
1180
1181
def duplicate(self):
    """duplicates the photo and returns Photo object for the duplicate"""
    dup_id = run_script("photoDuplicate", self.id)
    return Photo(dup_id)

export(export_path, original=False, overwrite=False, timeout=120, reveal_in_finder=False)

Export photo

Parameters:

Name Type Description Default
photo

Photo object to export

required
export_path

path to export to

required
original

if True, export original image, otherwise export current image; default = False

False
overwrite

if True, export will overwrite a file of same name as photo in export_path; default = False

False
timeout

number of seconds to wait for Photos to complete export before timing out; default = 120

120
reveal_in_finder

if True, will open Finder with exported items selected when done; default = False

False

Returns:

Type Description

List of full paths of exported photos. There may be more than one photo exported due

to live images and burst images.

Note: Photos always exports as high-quality JPEG unless original=True. If original=True, will export all burst images for burst photos and live movie for live photos. If original=False, only the primary image from a burst set will be exported for burst photos and the live movie component of a live image will not be exported, only the JPEG component.

Source code in photoscript/__init__.py
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
def export(
    self,
    export_path,
    original=False,
    overwrite=False,
    timeout=120,
    reveal_in_finder=False,
):
    """Export photo

    Args:
        photo: Photo object to export
        export_path: path to export to
        original: if True, export original image, otherwise export current image; default = False
        overwrite: if True, export will overwrite a file of same name as photo in export_path; default = False
        timeout: number of seconds to wait for Photos to complete export before timing out; default = 120
        reveal_in_finder: if True, will open Finder with exported items selected when done; default = False

    Returns:
        List of full paths of exported photos.  There may be more than one photo exported due
        to live images and burst images.

    Raises:
        ValueError if export_path is not a valid directory

    Note: Photos always exports as high-quality JPEG unless original=True.
    If original=True, will export all burst images for burst photos and
    live movie for live photos.  If original=False, only the primary image from a
    burst set will be exported for burst photos and the live movie component of a
    live image will not be exported, only the JPEG component.
    """
    return PhotosLibrary()._export_photo(
        self,
        export_path=export_path,
        original=original,
        overwrite=overwrite,
        timeout=timeout,
        reveal_in_finder=reveal_in_finder,
    )

spotlight()

spotlight the photo in Photos

Source code in photoscript/__init__.py
1183
1184
1185
def spotlight(self):
    """spotlight the photo in Photos"""
    run_script("photoSpotlight", self.id)

Album

Source code in photoscript/__init__.py
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
class Album:
    def __init__(self, uuid):
        # check to see if we need to add UUID suffix
        uuid, id_ = uuid_to_id(uuid, UUID_SUFFIX_ALBUM)
        if valuuidalbum := run_script("albumExists", id_):
            self.id = id_
            self._uuid = uuid
        else:
            raise ValueError(f"Invalid album id: {uuid}")

    @property
    def uuid(self):
        """UUID of Album (read only)"""
        return self._uuid

    @property
    def name(self):
        """name of album (read/write)"""
        name = run_script("albumName", self.id)
        return name if name != kMissingValue else ""

    @name.setter
    def name(self, name):
        """set name of album"""
        name = "" if name is None else name
        return run_script("albumSetName", self.id, name)

    @property
    def title(self):
        """title of album (alias for Album.name)"""
        return self.name

    @title.setter
    def title(self, title):
        """set title of album (alias for name)"""
        name = "" if title is None else title
        return run_script("albumSetName", self.id, name)

    @property
    def parent_id(self):
        """parent container id"""
        return run_script("albumParent", self.id)

    # TODO: if no parent should return a "My Albums" object that contains all top-level folders/albums
    @property
    def parent(self):
        """Return parent Folder object"""
        parent_id = self.parent_id
        if parent_id != 0:
            return Folder(parent_id)
        else:
            return None

    def path_str(self, delim="/"):
        """Return internal library path to album as string.
            e.g. "Folder/SubFolder/AlbumName"

        Args:
            delim: character to use as delimiter between path elements; default is "/"

        Raises:
            ValueError if delim is not a single character
        """
        if len(delim) > 1:
            raise ValueError("delim must be single character")

        return run_script("albumGetPath", self.id, delim)

    def photos(self):
        """list of Photo objects for photos contained in album"""
        photo_ids = run_script("albumPhotes", self.id)
        return [Photo(uuid) for uuid in photo_ids]

    def add(self, photos):
        """add photos from the library to album

        Args:
            photos: list of Photo objects to add to album

        Returns:
            list of Photo objects for added photos
        """
        uuids = [p.id for p in photos]
        added_ids = run_script("albumAdd", self.id, uuids)
        return [Photo(uuid) for uuid in added_ids]

    def import_photos(self, photo_paths, skip_duplicate_check=False):
        """import photos

        Args:
            photos: list of file paths to import
            skip_duplicate_check: if True, Photos will not check for duplicates on import, default is False

        Returns:
            list of Photo objects for imported photos
        """
        library = PhotosLibrary()
        return library.import_photos(
            photo_paths, album=self, skip_duplicate_check=skip_duplicate_check
        )

    def export(
        self,
        export_path,
        original=False,
        overwrite=False,
        timeout=120,
        reveal_in_finder=False,
    ):
        """Export photos in album to path

        Args:
            photo: Photo object to export
            export_path: path to export to
            original: if True, export original image, otherwise export current image; default = False
            overwrite: if True, export will overwrite a file of same name as photo in export_path; default = False
            timeout: number of seconds to wait for Photos to complete export (for each photo) before timing out; default = 120
            reveal_in_finder: if True, will open Finder with exported items selected when done; default = False

        Returns:
            List of full paths of exported photos.  There may be more than one photo exported due
            to live images and burst images.

        Raises:
            ValueError if export_path is not a valid directory

        Note: Photos always exports as high-quality JPEG unless original=True.
        If original=True, will export all burst images for burst photos and
        live movie for live photos.  If original=False, only the primary image from a
        burst set will be exported for burst photos and the live movie component of a
        live image will not be exported, only the JPEG component.
        """
        exported_photos = []
        for photo in self.photos():
            exported_photos.extend(
                photo.export(
                    export_path=export_path,
                    original=original,
                    overwrite=overwrite,
                    timeout=timeout,
                )
            )
        if reveal_in_finder and exported_photos:
            run_script("revealInFinder", exported_photos)
        return exported_photos

    def remove_by_id(self, photo_ids):
        """Remove photos from album.
            Note: Photos does not provide a way to remove photos from an album via AppleScript.
            This method actually creates a new Album with the same name as the original album and
            copies all photos from original album with exception of those to remove to the new album
            then deletes the old album.

        Args:
            photo_ids: list of photo ids to remove

        Returns:
            new Album object for the new album with photos removed.
        """
        photoslib = PhotosLibrary()
        new_album = photoslib.create_album(
            photoslib._temp_album_name(), folder=self.parent
        )
        old_photos = self.photos()
        new_photo_uuids = [
            photo.id for photo in old_photos if photo.id not in photo_ids
        ]
        new_photos = [Photo(uuid) for uuid in new_photo_uuids]
        if new_photos:
            new_album.add(new_photos)
        name = self.name
        photoslib.delete_album(self)
        new_album.name = name
        self.id = new_album.id
        self._uuid = new_album.uuid
        return new_album

    def remove(self, photos):
        """Remove photos from album.
            Note: Photos does not provide a way to remove photos from an album via AppleScript.
            This method actually creates a new Album with the same name as the original album and
            copies all photos from original album with exception of those to remove to the new album
            then deletes the old album.

        Args:
            photos: list of Photo objects to remove

        Returns:
            new Album object for the new album with photos removed.
        """
        photo_uuids = [photo.id for photo in photos]
        return self.remove_by_id(photo_uuids)

    def spotlight(self):
        """spotlight the album in Photos"""
        run_script("albumSpotlight", self.id)

    def __len__(self):
        return run_script("albumCount", self.id)

name property writable

name of album (read/write)

parent property

Return parent Folder object

parent_id property

parent container id

title property writable

title of album (alias for Album.name)

uuid property

UUID of Album (read only)

add(photos)

add photos from the library to album

Parameters:

Name Type Description Default
photos

list of Photo objects to add to album

required

Returns:

Type Description

list of Photo objects for added photos

Source code in photoscript/__init__.py
671
672
673
674
675
676
677
678
679
680
681
682
def add(self, photos):
    """add photos from the library to album

    Args:
        photos: list of Photo objects to add to album

    Returns:
        list of Photo objects for added photos
    """
    uuids = [p.id for p in photos]
    added_ids = run_script("albumAdd", self.id, uuids)
    return [Photo(uuid) for uuid in added_ids]

export(export_path, original=False, overwrite=False, timeout=120, reveal_in_finder=False)

Export photos in album to path

Parameters:

Name Type Description Default
photo

Photo object to export

required
export_path

path to export to

required
original

if True, export original image, otherwise export current image; default = False

False
overwrite

if True, export will overwrite a file of same name as photo in export_path; default = False

False
timeout

number of seconds to wait for Photos to complete export (for each photo) before timing out; default = 120

120
reveal_in_finder

if True, will open Finder with exported items selected when done; default = False

False

Returns:

Type Description

List of full paths of exported photos. There may be more than one photo exported due

to live images and burst images.

Note: Photos always exports as high-quality JPEG unless original=True. If original=True, will export all burst images for burst photos and live movie for live photos. If original=False, only the primary image from a burst set will be exported for burst photos and the live movie component of a live image will not be exported, only the JPEG component.

Source code in photoscript/__init__.py
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
def export(
    self,
    export_path,
    original=False,
    overwrite=False,
    timeout=120,
    reveal_in_finder=False,
):
    """Export photos in album to path

    Args:
        photo: Photo object to export
        export_path: path to export to
        original: if True, export original image, otherwise export current image; default = False
        overwrite: if True, export will overwrite a file of same name as photo in export_path; default = False
        timeout: number of seconds to wait for Photos to complete export (for each photo) before timing out; default = 120
        reveal_in_finder: if True, will open Finder with exported items selected when done; default = False

    Returns:
        List of full paths of exported photos.  There may be more than one photo exported due
        to live images and burst images.

    Raises:
        ValueError if export_path is not a valid directory

    Note: Photos always exports as high-quality JPEG unless original=True.
    If original=True, will export all burst images for burst photos and
    live movie for live photos.  If original=False, only the primary image from a
    burst set will be exported for burst photos and the live movie component of a
    live image will not be exported, only the JPEG component.
    """
    exported_photos = []
    for photo in self.photos():
        exported_photos.extend(
            photo.export(
                export_path=export_path,
                original=original,
                overwrite=overwrite,
                timeout=timeout,
            )
        )
    if reveal_in_finder and exported_photos:
        run_script("revealInFinder", exported_photos)
    return exported_photos

import_photos(photo_paths, skip_duplicate_check=False)

import photos

Parameters:

Name Type Description Default
photos

list of file paths to import

required
skip_duplicate_check

if True, Photos will not check for duplicates on import, default is False

False

Returns:

Type Description

list of Photo objects for imported photos

Source code in photoscript/__init__.py
684
685
686
687
688
689
690
691
692
693
694
695
696
697
def import_photos(self, photo_paths, skip_duplicate_check=False):
    """import photos

    Args:
        photos: list of file paths to import
        skip_duplicate_check: if True, Photos will not check for duplicates on import, default is False

    Returns:
        list of Photo objects for imported photos
    """
    library = PhotosLibrary()
    return library.import_photos(
        photo_paths, album=self, skip_duplicate_check=skip_duplicate_check
    )

path_str(delim='/')

Return internal library path to album as string. e.g. "Folder/SubFolder/AlbumName"

Parameters:

Name Type Description Default
delim

character to use as delimiter between path elements; default is "/"

'/'
Source code in photoscript/__init__.py
651
652
653
654
655
656
657
658
659
660
661
662
663
664
def path_str(self, delim="/"):
    """Return internal library path to album as string.
        e.g. "Folder/SubFolder/AlbumName"

    Args:
        delim: character to use as delimiter between path elements; default is "/"

    Raises:
        ValueError if delim is not a single character
    """
    if len(delim) > 1:
        raise ValueError("delim must be single character")

    return run_script("albumGetPath", self.id, delim)

photos()

list of Photo objects for photos contained in album

Source code in photoscript/__init__.py
666
667
668
669
def photos(self):
    """list of Photo objects for photos contained in album"""
    photo_ids = run_script("albumPhotes", self.id)
    return [Photo(uuid) for uuid in photo_ids]

remove(photos)

Remove photos from album. Note: Photos does not provide a way to remove photos from an album via AppleScript. This method actually creates a new Album with the same name as the original album and copies all photos from original album with exception of those to remove to the new album then deletes the old album.

Parameters:

Name Type Description Default
photos

list of Photo objects to remove

required

Returns:

Type Description

new Album object for the new album with photos removed.

Source code in photoscript/__init__.py
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
def remove(self, photos):
    """Remove photos from album.
        Note: Photos does not provide a way to remove photos from an album via AppleScript.
        This method actually creates a new Album with the same name as the original album and
        copies all photos from original album with exception of those to remove to the new album
        then deletes the old album.

    Args:
        photos: list of Photo objects to remove

    Returns:
        new Album object for the new album with photos removed.
    """
    photo_uuids = [photo.id for photo in photos]
    return self.remove_by_id(photo_uuids)

remove_by_id(photo_ids)

Remove photos from album. Note: Photos does not provide a way to remove photos from an album via AppleScript. This method actually creates a new Album with the same name as the original album and copies all photos from original album with exception of those to remove to the new album then deletes the old album.

Parameters:

Name Type Description Default
photo_ids

list of photo ids to remove

required

Returns:

Type Description

new Album object for the new album with photos removed.

Source code in photoscript/__init__.py
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
def remove_by_id(self, photo_ids):
    """Remove photos from album.
        Note: Photos does not provide a way to remove photos from an album via AppleScript.
        This method actually creates a new Album with the same name as the original album and
        copies all photos from original album with exception of those to remove to the new album
        then deletes the old album.

    Args:
        photo_ids: list of photo ids to remove

    Returns:
        new Album object for the new album with photos removed.
    """
    photoslib = PhotosLibrary()
    new_album = photoslib.create_album(
        photoslib._temp_album_name(), folder=self.parent
    )
    old_photos = self.photos()
    new_photo_uuids = [
        photo.id for photo in old_photos if photo.id not in photo_ids
    ]
    new_photos = [Photo(uuid) for uuid in new_photo_uuids]
    if new_photos:
        new_album.add(new_photos)
    name = self.name
    photoslib.delete_album(self)
    new_album.name = name
    self.id = new_album.id
    self._uuid = new_album.uuid
    return new_album

spotlight()

spotlight the album in Photos

Source code in photoscript/__init__.py
791
792
793
def spotlight(self):
    """spotlight the album in Photos"""
    run_script("albumSpotlight", self.id)

Folder

Source code in photoscript/__init__.py
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
class Folder:
    def __init__(
        self,
        uuid: str | None = None,
        path: list[str] | None = None,
        idstring: str | None = None,
    ):
        """Create a Folder object; only one of path, uuid, or idstring should be specified

        The preferred method is to use the path argument or idstring to specify the folder
        as this is much faster than using uuid.  The uuid argument is listed first for
        backwards compatibility.

        Args:
            path: list of folder names in descending order from parent to child: ["Folder", "SubFolder"]
            uuid: uuid of folder: "E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D/L0/020" or "E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D"
            idstring: idstring of folder:
                "folder id(\"E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D/L0/020\") of folder id(\"CB051A4C-2CB7-4B90-B59B-08CC4D0C2823/L0/020\")"
        """
        if sum(bool(x) for x in (path, uuid, idstring)) != 1:
            raise ValueError(
                "One (and only one) of path, uuid, or idstring must be specified"
            )

        if uuid is not None:
            uuid, _id = uuid_to_id(uuid, UUID_SUFFIX_FOLDER)
        else:
            _id = None

        self._path, self._uuid, self._id, self._idstring = path, uuid, _id, idstring

        # if initialized with path or uuid, need to initialize idstring
        if self._path is not None:
            self._idstring = run_script("folderGetIDStringFromPath", self._path)
            if self._idstring == kMissingValue:
                raise ValueError(f"Folder at path {self._path} does not exist")
        elif self._id is not None:
            # if uuid was passed, _id will have been initialized above
            # second argument is False so search is not limited to top-level folders
            self._idstring = run_script(
                "photosLibraryGetFolderIDStringForID", self._id, False
            )
            if self._idstring == kMissingValue:
                raise ValueError(f"Folder id {self._id} does not exist")

        if not run_script("folderExists", self._idstring):
            raise ValueError(f"Folder {self._idstring} does not exist")

    @property
    def idstring(self) -> str:
        """idstring of folder"""
        return self._idstring

    @property
    def uuid(self):
        """UUID of folder"""
        if self._uuid is not None:
            return self._uuid
        self._uuid, self._id = uuid_to_id(
            run_script("folderUUID", self._idstring), UUID_SUFFIX_FOLDER
        )
        return self._uuid

    @property
    def id(self):
        """ID of folder"""
        if self._id is not None:
            return self._id
        self._uuid, self._id = uuid_to_id(
            run_script("folderUUID", self._idstring), UUID_SUFFIX_FOLDER
        )
        return self._id

    @property
    def name(self):
        """name of folder (read/write)"""
        name = run_script("folderName", self._idstring)
        return name if name != kMissingValue else ""

    @name.setter
    def name(self, name):
        """set name of photo"""
        name = "" if name is None else name
        return run_script("folderSetName", self._idstring, name)

    @property
    def title(self):
        """title of folder (alias for Folder.name)"""
        return self.name

    @title.setter
    def title(self, title):
        """set title of folder (alias for name)"""
        name = "" if title is None else title
        return run_script("folderSetName", self._idstring, name)

    @property
    def parent_id(self):
        """parent container id string"""
        parent_id = run_script("folderParent", self._idstring)
        return parent_id if parent_id != kMissingValue else None

    # TODO: if no parent should return a "My Albums" object that contains all top-level folders/albums?
    @property
    def parent(self):
        """Return parent Folder object"""
        parent_idstring = self.parent_id
        return Folder(idstring=parent_idstring) if parent_idstring is not None else None

    def path_str(self, delim="/"):
        """Return internal library path to folder as string.
            e.g. "Folder/SubFolder"

        Args:
            delim: character to use as delimiter between path elements; default is "/"

        Raises:
            ValueError if delim is not a single character
        """
        if len(delim) > 1:
            raise ValueError("delim must be single character")

        return run_script("folderGetPath", self._idstring, delim)

    def path(self):
        """Return list of Folder objects this folder is contained in.
        path()[0] is the top-level folder this folder is contained in and
        path()[-1] is the immediate parent of this folder.  Returns empty
        list if folder is not contained in another folders.
        """
        folder_path = run_script("folderGetPathFolderIDScript", self._idstring)
        return [Folder(idstring=folder) for folder in folder_path]

    @property
    def albums(self):
        """list of Album objects for albums contained in folder"""
        album_ids = run_script("folderAlbums", self._idstring)
        return [Album(uuid) for uuid in album_ids]

    def album(self, name):
        """Return Album object contained in this folder for album named name
        or None if no matching album
        """
        return next((album for album in self.albums if album.name == name), None)

    @property
    def subfolders(self):
        """list of Folder objects for immediate sub-folders contained in folder"""
        folder_idstrings = run_script("folderFolders", self._idstring)
        return [Folder(idstring=ids) for ids in folder_idstrings]

    def folder(self, name):
        """Folder object for first subfolder folder named name.

        Args:
            name: name of folder to to return

        Returns:
            Folder object for first subfolder who's name matches name or None if not found
        """
        return next((folder for folder in self.subfolders if folder.name == name), None)

    def create_album(self, name: str) -> "Album":
        """Creates an album in this folder

        Args:
            name: name of new album

        Returns:
            Album object for newly created album
        """
        return PhotosLibrary().create_album(name=name, folder=self)

    def create_folder(self, name: str) -> "Folder":
        """creates a folder in this folder

        Returns:
            Folder object for newly created folder
        """
        return PhotosLibrary().create_folder(name=name, folder=self)

    def spotlight(self):
        """spotlight the folder in Photos"""
        run_script("folderSpotlight", self._idstring)

    def __len__(self):
        return run_script("folderCount", self._idstring)

albums property

list of Album objects for albums contained in folder

id property

ID of folder

idstring: str property

idstring of folder

name property writable

name of folder (read/write)

parent property

Return parent Folder object

parent_id property

parent container id string

subfolders property

list of Folder objects for immediate sub-folders contained in folder

title property writable

title of folder (alias for Folder.name)

uuid property

UUID of folder

__init__(uuid=None, path=None, idstring=None)

Create a Folder object; only one of path, uuid, or idstring should be specified

The preferred method is to use the path argument or idstring to specify the folder as this is much faster than using uuid. The uuid argument is listed first for backwards compatibility.

Parameters:

Name Type Description Default
path list[str] | None

list of folder names in descending order from parent to child: ["Folder", "SubFolder"]

None
uuid str | None

uuid of folder: "E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D/L0/020" or "E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D"

None
idstring str | None

idstring of folder: "folder id("E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D/L0/020") of folder id("CB051A4C-2CB7-4B90-B59B-08CC4D0C2823/L0/020")"

None
Source code in photoscript/__init__.py
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
def __init__(
    self,
    uuid: str | None = None,
    path: list[str] | None = None,
    idstring: str | None = None,
):
    """Create a Folder object; only one of path, uuid, or idstring should be specified

    The preferred method is to use the path argument or idstring to specify the folder
    as this is much faster than using uuid.  The uuid argument is listed first for
    backwards compatibility.

    Args:
        path: list of folder names in descending order from parent to child: ["Folder", "SubFolder"]
        uuid: uuid of folder: "E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D/L0/020" or "E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D"
        idstring: idstring of folder:
            "folder id(\"E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D/L0/020\") of folder id(\"CB051A4C-2CB7-4B90-B59B-08CC4D0C2823/L0/020\")"
    """
    if sum(bool(x) for x in (path, uuid, idstring)) != 1:
        raise ValueError(
            "One (and only one) of path, uuid, or idstring must be specified"
        )

    if uuid is not None:
        uuid, _id = uuid_to_id(uuid, UUID_SUFFIX_FOLDER)
    else:
        _id = None

    self._path, self._uuid, self._id, self._idstring = path, uuid, _id, idstring

    # if initialized with path or uuid, need to initialize idstring
    if self._path is not None:
        self._idstring = run_script("folderGetIDStringFromPath", self._path)
        if self._idstring == kMissingValue:
            raise ValueError(f"Folder at path {self._path} does not exist")
    elif self._id is not None:
        # if uuid was passed, _id will have been initialized above
        # second argument is False so search is not limited to top-level folders
        self._idstring = run_script(
            "photosLibraryGetFolderIDStringForID", self._id, False
        )
        if self._idstring == kMissingValue:
            raise ValueError(f"Folder id {self._id} does not exist")

    if not run_script("folderExists", self._idstring):
        raise ValueError(f"Folder {self._idstring} does not exist")

album(name)

Return Album object contained in this folder for album named name or None if no matching album

Source code in photoscript/__init__.py
938
939
940
941
942
def album(self, name):
    """Return Album object contained in this folder for album named name
    or None if no matching album
    """
    return next((album for album in self.albums if album.name == name), None)

create_album(name)

Creates an album in this folder

Parameters:

Name Type Description Default
name str

name of new album

required

Returns:

Type Description
'Album'

Album object for newly created album

Source code in photoscript/__init__.py
961
962
963
964
965
966
967
968
969
970
def create_album(self, name: str) -> "Album":
    """Creates an album in this folder

    Args:
        name: name of new album

    Returns:
        Album object for newly created album
    """
    return PhotosLibrary().create_album(name=name, folder=self)

create_folder(name)

creates a folder in this folder

Returns:

Type Description
'Folder'

Folder object for newly created folder

Source code in photoscript/__init__.py
972
973
974
975
976
977
978
def create_folder(self, name: str) -> "Folder":
    """creates a folder in this folder

    Returns:
        Folder object for newly created folder
    """
    return PhotosLibrary().create_folder(name=name, folder=self)

folder(name)

Folder object for first subfolder folder named name.

Parameters:

Name Type Description Default
name

name of folder to to return

required

Returns:

Type Description

Folder object for first subfolder who's name matches name or None if not found

Source code in photoscript/__init__.py
950
951
952
953
954
955
956
957
958
959
def folder(self, name):
    """Folder object for first subfolder folder named name.

    Args:
        name: name of folder to to return

    Returns:
        Folder object for first subfolder who's name matches name or None if not found
    """
    return next((folder for folder in self.subfolders if folder.name == name), None)

path()

Return list of Folder objects this folder is contained in. path()[0] is the top-level folder this folder is contained in and path()[-1] is the immediate parent of this folder. Returns empty list if folder is not contained in another folders.

Source code in photoscript/__init__.py
923
924
925
926
927
928
929
930
def path(self):
    """Return list of Folder objects this folder is contained in.
    path()[0] is the top-level folder this folder is contained in and
    path()[-1] is the immediate parent of this folder.  Returns empty
    list if folder is not contained in another folders.
    """
    folder_path = run_script("folderGetPathFolderIDScript", self._idstring)
    return [Folder(idstring=folder) for folder in folder_path]

path_str(delim='/')

Return internal library path to folder as string. e.g. "Folder/SubFolder"

Parameters:

Name Type Description Default
delim

character to use as delimiter between path elements; default is "/"

'/'
Source code in photoscript/__init__.py
908
909
910
911
912
913
914
915
916
917
918
919
920
921
def path_str(self, delim="/"):
    """Return internal library path to folder as string.
        e.g. "Folder/SubFolder"

    Args:
        delim: character to use as delimiter between path elements; default is "/"

    Raises:
        ValueError if delim is not a single character
    """
    if len(delim) > 1:
        raise ValueError("delim must be single character")

    return run_script("folderGetPath", self._idstring, delim)

spotlight()

spotlight the folder in Photos

Source code in photoscript/__init__.py
980
981
982
def spotlight(self):
    """spotlight the folder in Photos"""
    run_script("folderSpotlight", self._idstring)