Added strip_live.py example (#747)
This commit is contained in:
parent
28e03ee86a
commit
3d83e184b8
196
examples/strip_live.py
Normal file
196
examples/strip_live.py
Normal file
@ -0,0 +1,196 @@
|
||||
"""Export selected Live photos and re-import just the image portion"""
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import List
|
||||
|
||||
import click
|
||||
from photoscript import Album, Photo, PhotosLibrary
|
||||
from rich import print
|
||||
|
||||
from osxphotos import PhotoInfo, PhotosDB
|
||||
|
||||
DEFAULT_DELETE_ALBUM = "Live Photos to Delete"
|
||||
DEFAULT_NEW_ALBUM = "Imported Live Photos"
|
||||
|
||||
|
||||
def rename_photos(photo_paths: List[str]) -> List[str]:
|
||||
"""Given a list of photo paths, rename the photos so names don't clash as duplicated on re-import"""
|
||||
# use perf_counter_ns as a simple unique ID to ensure each photo has a different name
|
||||
new_paths = []
|
||||
for path in photo_paths:
|
||||
path = pathlib.Path(path)
|
||||
stem = f"{path.stem}_{time.perf_counter_ns()}"
|
||||
new_path = path.rename(path.parent / f"{stem}{path.suffix}")
|
||||
new_paths.append(str(new_path))
|
||||
return new_paths
|
||||
|
||||
|
||||
def set_metadata_from_photo(source_photo: PhotoInfo, dest_photos: List[Photo]):
|
||||
"""Set metadata (keywords, albums, title, description, favorite) for dest_photos from source_photo"""
|
||||
title = source_photo.title
|
||||
description = source_photo.description
|
||||
keywords = source_photo.keywords
|
||||
favorite = source_photo.favorite
|
||||
|
||||
# apply metadata to each photo
|
||||
for dest_photo in dest_photos:
|
||||
dest_photo.title = title
|
||||
dest_photo.description = description
|
||||
dest_photo.keywords = keywords
|
||||
dest_photo.favorite = favorite
|
||||
|
||||
# add photos to albums
|
||||
album_ids = [a.uuid for a in source_photo.album_info]
|
||||
for album_id in album_ids:
|
||||
album = Album(album_id)
|
||||
album.add(dest_photos)
|
||||
|
||||
|
||||
def process_photo(
|
||||
photo: Photo,
|
||||
photosdb: PhotosDB,
|
||||
keep_originals: bool,
|
||||
download_missing: bool,
|
||||
new_album: Album,
|
||||
delete_album: Album,
|
||||
):
|
||||
"""Process each Live Photo to export/re-import it"""
|
||||
with TemporaryDirectory() as tempdir:
|
||||
p = photosdb.get_photo(photo.uuid)
|
||||
if not p.live_photo:
|
||||
print(
|
||||
f"[yellow]Skipping non-Live photo {p.original_filename} ({p.uuid})[/]"
|
||||
)
|
||||
return
|
||||
|
||||
# versions to download (True for edited, False for original)
|
||||
versions = []
|
||||
|
||||
# use photos_export to download from iCloud
|
||||
photos_export = False
|
||||
|
||||
# try to download missing photos only if photo is missing and --download-missing
|
||||
if keep_originals or not p.hasadjustments:
|
||||
# export original photo
|
||||
if not p.path and not download_missing:
|
||||
print(
|
||||
f"[yellow]Skipping missing original version of photo {p.original_filename} ({p.uuid}) (you may want to try --download-missing)[/]"
|
||||
)
|
||||
return
|
||||
photos_export = download_missing and not p.path
|
||||
versions.append(False)
|
||||
|
||||
if p.hasadjustments:
|
||||
if not p.path_edited and not download_missing:
|
||||
print(
|
||||
f"[yellow]Skipping missing edited version of photo {p.original_filename} ({p.uuid}) (you may want to try --download-missing)[/]"
|
||||
)
|
||||
return
|
||||
photos_export = photos_export or (download_missing and not p.path_edited)
|
||||
versions.append(True)
|
||||
|
||||
exported = []
|
||||
for version in versions:
|
||||
# export the actual photo (without the Live video)
|
||||
print(
|
||||
f"Exporting {'edited' if version else 'original'} photo {p.original_filename} ({p.uuid})"
|
||||
)
|
||||
if exports := p.export(
|
||||
tempdir,
|
||||
live_photo=False,
|
||||
edited=version,
|
||||
use_photos_export=photos_export,
|
||||
):
|
||||
exported.extend(exports)
|
||||
else:
|
||||
print(
|
||||
f"[red]Error exporting photo {p.original_filename} ({p.uuid})[/]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if not exported:
|
||||
return
|
||||
|
||||
exported = rename_photos(exported)
|
||||
print(
|
||||
f"Re-importing {', '.join([pathlib.Path(p).name for p in exported])} to album '{new_album.name}'"
|
||||
)
|
||||
new_photos = new_album.import_photos(exported)
|
||||
|
||||
print("Applying metadata to newly imported photos")
|
||||
set_metadata_from_photo(p, new_photos)
|
||||
|
||||
print(f"Moving {p.original_filename} to album '{delete_album.name}'")
|
||||
delete_album.add([Photo(p.uuid)])
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--download-missing", is_flag=True, help="Download missing files from iCloud."
|
||||
)
|
||||
@click.option(
|
||||
"--keep-originals",
|
||||
is_flag=True,
|
||||
help="If photo is edited, also keep the original, unedited photo. "
|
||||
"Without --keep-originals, only the edited version of a Live photo that has been edited will be kept.",
|
||||
)
|
||||
@click.option(
|
||||
"--delete-album",
|
||||
"delete_album_name",
|
||||
default=DEFAULT_DELETE_ALBUM,
|
||||
help="Album to put Live photos in when they're ready to be deleted; "
|
||||
f"default = '{DEFAULT_DELETE_ALBUM}'",
|
||||
)
|
||||
@click.option(
|
||||
"--new-album",
|
||||
"new_album_name",
|
||||
default=DEFAULT_NEW_ALBUM,
|
||||
help="Album to put Live photos in when they've been re-imported after stripping the video component; "
|
||||
f"default = '{DEFAULT_NEW_ALBUM}'",
|
||||
)
|
||||
def strip_live_photos(
|
||||
download_missing, keep_originals, delete_album_name, new_album_name
|
||||
):
|
||||
"""Export selected Live photos and re-import just the image portion.
|
||||
|
||||
This script can be used to free space in your Photos library by allowing you
|
||||
to effectively delete just the Live video portion of a Live photo.
|
||||
|
||||
The photo part of the Live photo will be exported to a temporary directory then
|
||||
reimported into Photos. Albums, keywords, title/caption, favorite, and description
|
||||
will be preserved. Unfortunately person/face data cannot be preserved.
|
||||
|
||||
After export Live photos will be moved to an album (which can be set using
|
||||
--delete-album) so they can be deleted. You can use Command + Delete to put the
|
||||
photos in the trash after selecting them in the album.
|
||||
"""
|
||||
photoslib = PhotosLibrary()
|
||||
selected = photoslib.selection
|
||||
if not selected:
|
||||
print("No photos selected...nothing to do", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Processing {len(selected)} photo(s)")
|
||||
print("Loading Photos database")
|
||||
photosdb = PhotosDB()
|
||||
|
||||
new_album = photoslib.album(
|
||||
new_album_name, top_level=True
|
||||
) or photoslib.create_album(new_album_name)
|
||||
delete_album = photoslib.album(
|
||||
delete_album_name, top_level=True
|
||||
) or photoslib.create_album(delete_album_name)
|
||||
|
||||
for photo in selected:
|
||||
process_photo(
|
||||
photo, photosdb, keep_originals, download_missing, new_album, delete_album
|
||||
)
|
||||
|
||||
new_album.spotlight()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strip_live_photos()
|
||||
Loading…
x
Reference in New Issue
Block a user