parent
f5ed3d7518
commit
00481d3623
150
examples/find_bad_extensions.py
Normal file
150
examples/find_bad_extensions.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
"""Scan Photos library to find photos with bad (incorrect) file extensions.
|
||||||
|
|
||||||
|
This can be run with osxphotos via: osxphotos run find_bad_extensions.py
|
||||||
|
|
||||||
|
For help, run: osxphotos run find_bad_extensions.py --help
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
from osxphotos import PhotoInfo, PhotosDB
|
||||||
|
from osxphotos.cli.common import get_data_dir
|
||||||
|
from osxphotos.exiftool import ExifTool, get_exiftool_path
|
||||||
|
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||||
|
|
||||||
|
|
||||||
|
def check_extension(filepath: str) -> tuple[bool, str, str]:
|
||||||
|
"""Check if file extension is correct for image file using exiftool
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: path to file to check
|
||||||
|
|
||||||
|
Returns: tuple of (bool, str, str) where bool is True if extension is correct, False if not
|
||||||
|
and str, str is the current extension, correct extension or current extension if correct
|
||||||
|
"""
|
||||||
|
filepath = pathlib.Path(filepath)
|
||||||
|
current_ext = filepath.suffix.lower()
|
||||||
|
|
||||||
|
current_ext = current_ext[1:] if current_ext else "" # remove leading dot
|
||||||
|
exiftool = ExifTool(filepath)
|
||||||
|
correct_ext = exiftool.asdict().get("File:FileTypeExtension").lower()
|
||||||
|
if current_ext != correct_ext:
|
||||||
|
# there are some extensions that have more than one valid extension
|
||||||
|
# there are likely more but these are the ones I've seen so far
|
||||||
|
is_correct = (
|
||||||
|
current_ext in ("jpg", "jpeg") and correct_ext in ("jpg", "jpeg")
|
||||||
|
) or (current_ext in ("tif", "tiff") and correct_ext in ("tif", "tiff"))
|
||||||
|
else:
|
||||||
|
is_correct = True
|
||||||
|
return is_correct, current_ext, correct_ext
|
||||||
|
|
||||||
|
|
||||||
|
def check_photo(
|
||||||
|
photo: PhotoInfo, recheck: bool, version: str, kvstore: SQLiteKVStore
|
||||||
|
) -> None:
|
||||||
|
"""Check PhotoInfo for correct extension
|
||||||
|
|
||||||
|
Args:
|
||||||
|
photo: PhotoInfo instance
|
||||||
|
recheck: if True, recheck even if previously checked
|
||||||
|
version: "original" or "edited"
|
||||||
|
kvstore: SQLiteKVStore instance to store results
|
||||||
|
"""
|
||||||
|
photo_path = photo.path if version == "original" else photo.path_edited
|
||||||
|
if photo_path is None:
|
||||||
|
print(
|
||||||
|
f":warning-emoji: [yellow]No {version} path for photo: {photo.original_filename} ({photo.uuid})",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if recheck or f"{photo.uuid}:{version}" not in kvstore:
|
||||||
|
is_correct, current_ext, correct_ext = check_extension(photo_path)
|
||||||
|
if not is_correct:
|
||||||
|
print(
|
||||||
|
f"{photo.original_filename} ({version}) has incorrect extension: [red]{current_ext}[/] should be [green]{correct_ext}[/]",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
# output results as CSV to stdout
|
||||||
|
csv.writer(sys.stdout).writerow(
|
||||||
|
[
|
||||||
|
photo.uuid,
|
||||||
|
photo.original_filename,
|
||||||
|
version,
|
||||||
|
current_ext,
|
||||||
|
correct_ext,
|
||||||
|
photo_path,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
kvstore[f"{photo.uuid}:{version}"] = (is_correct, current_ext, correct_ext)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--library",
|
||||||
|
default=None,
|
||||||
|
type=click.Path(exists=True, file_okay=True, dir_okay=True),
|
||||||
|
help="Path to Photos library to use. Default is to use default Photos library.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--recheck",
|
||||||
|
is_flag=True,
|
||||||
|
help="Recheck all files even if previously checked and cached.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--edited",
|
||||||
|
is_flag=True,
|
||||||
|
help="Check edited versions of photos in addition to originals.",
|
||||||
|
)
|
||||||
|
def main(library: str, recheck: bool, edited: bool):
|
||||||
|
"""Scan Photos library to find photos with bad (incorrect) file extensions.
|
||||||
|
|
||||||
|
This can be run with osxphotos via: `osxphotos run find_bad_extensions.py`
|
||||||
|
|
||||||
|
Both STDOUT and STDERR are used to output results.
|
||||||
|
|
||||||
|
STDOUT is used to output a CSV file with the following columns:
|
||||||
|
|
||||||
|
uuid, original_filename, version, current_extension, correct_extension, path
|
||||||
|
|
||||||
|
Thus, to save the results to a file, run:
|
||||||
|
|
||||||
|
osxphotos run find_bad_extensions.py > results.csv
|
||||||
|
"""
|
||||||
|
|
||||||
|
# exiftool required to run
|
||||||
|
try:
|
||||||
|
get_exiftool_path()
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(
|
||||||
|
":cross_mark-emoji: [red]Could not find exiftool. Please download and install"
|
||||||
|
" from https://exiftool.org/",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
raise click.Abort() from e
|
||||||
|
|
||||||
|
# path to the cache database to store results of extension check
|
||||||
|
cache_db_path = os.path.join(get_data_dir(), "bad_extensions.db")
|
||||||
|
kvstore = SQLiteKVStore(
|
||||||
|
cache_db_path, wal=True, serialize=json.dumps, deserialize=json.loads
|
||||||
|
)
|
||||||
|
click.echo(f"Using cache database: [blue]{cache_db_path}", err=True)
|
||||||
|
|
||||||
|
# load the Photos database and check each photo
|
||||||
|
photosdb = PhotosDB(dbfile=library)
|
||||||
|
for photo in photosdb.photos():
|
||||||
|
check_photo(photo, recheck, "original", kvstore)
|
||||||
|
if edited and photo.hasadjustments:
|
||||||
|
check_photo(photo, recheck, "edited", kvstore)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
44
examples/fix_export_extension.py
Normal file
44
examples/fix_export_extension.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
""" Example function for use with osxphotos export --post-function option """
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from osxphotos import ExportResults, PhotoInfo
|
||||||
|
from osxphotos.exiftool import ExifTool
|
||||||
|
|
||||||
|
|
||||||
|
def fix_extension(
|
||||||
|
photo: PhotoInfo, results: ExportResults, verbose: Callable, **kwargs
|
||||||
|
):
|
||||||
|
"""Call this with osxphotos export /path/to/export --post-function fix_export_extension.py::fix_extension
|
||||||
|
This will get called immediately after the photo has been exported
|
||||||
|
|
||||||
|
See full example here: https://github.com/RhetTbull/osxphotos/blob/master/examples/post_function.py
|
||||||
|
|
||||||
|
Args:
|
||||||
|
photo: PhotoInfo instance for the photo that's just been exported
|
||||||
|
results: ExportResults instance with information about the files associated with the exported photo
|
||||||
|
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
|
||||||
|
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Use verbose(str) instead of print if you want your function to conditionally output text depending on --verbose flag
|
||||||
|
Any string printed with verbose that contains "warning" or "error" (case-insensitive) will be printed with the appropriate warning or error color
|
||||||
|
Will not be called if --dry-run flag is enabled
|
||||||
|
Will be called immediately after export and before any --post-command commands are executed
|
||||||
|
"""
|
||||||
|
|
||||||
|
for filepath in results.exported:
|
||||||
|
filepath = pathlib.Path(filepath)
|
||||||
|
ext = filepath.suffix.lower()
|
||||||
|
if not ext:
|
||||||
|
continue
|
||||||
|
ext = ext[1:] # remove leading dot
|
||||||
|
exiftool = ExifTool(filepath)
|
||||||
|
actual_ext = exiftool.asdict().get("File:FileTypeExtension").lower()
|
||||||
|
if ext != actual_ext and (ext not in ("jpg", "jpeg") or actual_ext != "jpg"):
|
||||||
|
# WARNING: Does not check for name collisions; left as an exercise for the reader
|
||||||
|
verbose(f"Fixing extension for {filepath} from {ext} to {actual_ext}")
|
||||||
|
new_filepath = filepath.with_suffix(f".{actual_ext}")
|
||||||
|
verbose(f"Renaming {filepath} to {new_filepath}")
|
||||||
|
filepath.rename(new_filepath)
|
||||||
Loading…
x
Reference in New Issue
Block a user