Coverage for src/photo_path_utils.py: 100%
36 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-16 04:41 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-16 04:41 +0000
1"""Photo path utils
2 Extract filename and extension from photo.
4 Args:
5 photo: Photo object from iCloudPy
6 file_size: File size variant (original, medium, thumb, etc.)
8 Returns:
9 Tuple of (name, extension) where name is filename without extension
10 and extension is the file extension.
12This module contains utilities for generating photo file paths and managing
13file naming conventions for photo synchronization.
14"""
16___author___ = "Mandar Patil <mandarons@pm.me>"
18import base64
19import os
20import unicodedata
22from src import get_logger
24LOGGER = get_logger()
27def get_photo_name_and_extension(photo, file_size: str) -> tuple[str, str]:
28 """Extract filename and extension from photo.
30 Args:
31 photo: Photo object from iCloudPy
32 file_size: File size variant (original, medium, thumb, etc.)
34 Returns:
35 Tuple of (name, extension) where name is filename without extension
36 and extension is the file extension
37 """
38 filename = photo.filename
39 name, extension = filename.rsplit(".", 1) if "." in filename else [filename, ""]
41 # Handle original_alt file type mapping
42 if file_size == "original_alt" and file_size in photo.versions:
43 filetype = photo.versions[file_size]["type"]
44 if filetype in _get_original_alt_filetype_mapping():
45 extension = _get_original_alt_filetype_mapping()[filetype]
46 else:
47 LOGGER.warning(f"Unknown filetype {filetype} for original_alt version of {filename}")
49 return name, extension
52def generate_photo_filename_with_metadata(photo, file_size: str) -> str:
53 """Generate filename with file size and photo ID metadata.
55 Args:
56 photo: Photo object from iCloudPy
57 file_size: File size variant (original, medium, thumb, etc.)
59 Returns:
60 Filename string with format: name__filesize__base64id.extension
61 """
62 name, extension = get_photo_name_and_extension(photo, file_size)
63 photo_id_encoded = base64.urlsafe_b64encode(photo.id.encode()).decode()
65 if extension == "":
66 return f"{'__'.join([name, file_size, photo_id_encoded])}"
67 else:
68 return f"{'__'.join([name, file_size, photo_id_encoded])}.{extension}"
71def create_folder_path_if_needed(destination_path: str, folder_format: str | None, photo) -> str:
72 """Create folder path based on folder format and photo creation date.
74 Args:
75 destination_path: Base destination path
76 folder_format: strftime format string for folder creation (e.g., "%Y/%m")
77 photo: Photo object with created date
79 Returns:
80 Full destination path including created folder if folder_format is specified
81 """
82 if folder_format is None:
83 return destination_path
85 folder = photo.created.strftime(folder_format)
86 full_destination = os.path.join(destination_path, folder)
87 os.makedirs(full_destination, exist_ok=True)
88 return full_destination
91def normalize_file_path(file_path: str) -> str:
92 """Normalize file path using Unicode NFC normalization.
94 Args:
95 file_path: File path to normalize
97 Returns:
98 Normalized file path
99 """
100 return unicodedata.normalize("NFC", file_path)
103def rename_legacy_file_if_exists(old_path: str, new_path: str) -> None:
104 """Rename legacy file format to new format if it exists.
106 Args:
107 old_path: Path to legacy file format
108 new_path: Path to new file format
109 """
110 import os
112 if os.path.isfile(old_path):
113 os.rename(old_path, new_path)
116def _get_original_alt_filetype_mapping() -> dict:
117 """Get mapping of original_alt file types to extensions.
119 Returns:
120 Dictionary mapping file types to extensions
121 """
122 return {
123 "public.png": "png",
124 "public.jpeg": "jpeg",
125 "public.heic": "heic",
126 "public.image": "HEIC",
127 "com.sony.arw-raw-image": "arw",
128 "org.webmproject.webp": "webp",
129 "com.compuserve.gif": "gif",
130 "com.adobe.raw-image": "dng",
131 "public.tiff": "tiff",
132 "public.jpeg-2000": "jp2",
133 "com.truevision.tga-image": "tga",
134 "com.sgi.sgi-image": "sgi",
135 "com.adobe.photoshop-image": "psd",
136 "public.pbm": "pbm",
137 "public.heif": "heif",
138 "com.microsoft.bmp": "bmp",
139 "com.fuji.raw-image": "raf",
140 "com.canon.cr2-raw-image": "cr2",
141 "com.panasonic.rw2-raw-image": "rw2",
142 "com.nikon.nrw-raw-image": "nrw",
143 "com.pentax.raw-image": "pef",
144 "com.nikon.raw-image": "nef",
145 "com.olympus.raw-image": "orf",
146 "com.adobe.pdf": "pdf",
147 "com.canon.cr3-raw-image": "cr3",
148 "com.olympus.or-raw-image": "orf",
149 "public.mpo-image": "mpo",
150 "com.dji.mimo.pano.jpeg": "jpg",
151 "public.avif": "avif",
152 "com.canon.crw-raw-image": "crw",
153 }