Skip to content

Commit

Permalink
Add support for folder_format config option
Browse files Browse the repository at this point in the history
  • Loading branch information
tymmej committed Oct 7, 2023
1 parent dc02947 commit aa4dddf
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ photos:
remove_obsolete: false
sync_interval: 500
all_albums: false # Optional, default false. If true preserve album structure. If same photo is in multpile albums creates duplicates on filesystem
folder_format: "%Y/%m" # optional, if set put photos in subfolders according to format
filters:
# if all_albums is false list of albums to download, if all_albums is true list of ignored albums
# if empty and all_albums is false download all photos to "all" folder. if empty and all_albums is true download all folders
Expand Down
10 changes: 10 additions & 0 deletions src/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,13 @@ def get_region(config):
region = "global"

return region


def get_folder_format(config):
"""Return filename format or None."""
fmt = None
config_path = ["photos", "folder_format"]
if traverse_config_path(config=config, config_path=config_path):
fmt = get_config_value(config=config, config_path=config_path)
LOGGER.info(f"Using format {fmt}.")
return fmt
26 changes: 21 additions & 5 deletions src/sync_photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def photo_wanted(photo, extensions):
return False


def generate_file_name(photo, file_size, destination_path):
def generate_file_name(photo, file_size, destination_path, folder_fmt):
"""Generate full path to file."""
filename = photo.filename
name, extension = filename.rsplit(".", 1) if "." in filename else [filename, ""]
Expand All @@ -40,6 +40,17 @@ def generate_file_name(photo, file_size, destination_path):
else f'{"__".join([name, file_size, base64.urlsafe_b64encode(photo.id.encode()).decode()])}.{extension}',
)

if folder_fmt is not None:
folder = photo.created.strftime(folder_fmt)
file_size_id_path = os.path.join(
destination_path,
folder,
f'{"__".join([name, file_size, base64.urlsafe_b64encode(photo.id.encode()).decode()])}'
if extension == ""
else f'{"__".join([name, file_size, base64.urlsafe_b64encode(photo.id.encode()).decode()])}.{extension}',
)
os.makedirs(os.path.join(destination_path, folder), exist_ok=True)

file_size_id_path_norm = unicodedata.normalize("NFC", file_size_id_path)

if os.path.isfile(file_path):
Expand Down Expand Up @@ -83,10 +94,10 @@ def download_photo(photo, file_size, destination_path):
return True


def process_photo(photo, file_size, destination_path, files):
def process_photo(photo, file_size, destination_path, files, folder_fmt):
"""Process photo details."""
photo_path = generate_file_name(
photo=photo, file_size=file_size, destination_path=destination_path
photo=photo, file_size=file_size, destination_path=destination_path, folder_fmt=folder_fmt
)
if file_size not in photo.versions:
LOGGER.warning(
Expand All @@ -101,7 +112,7 @@ def process_photo(photo, file_size, destination_path, files):
return True


def sync_album(album, destination_path, file_sizes, extensions=None, files=None):
def sync_album(album, destination_path, file_sizes, extensions=None, files=None, folder_fmt=None):
"""Sync given album."""
if album is None or destination_path is None or file_sizes is None:
return None
Expand All @@ -110,7 +121,7 @@ def sync_album(album, destination_path, file_sizes, extensions=None, files=None)
for photo in album:
if photo_wanted(photo, extensions):
for file_size in file_sizes:
process_photo(photo, file_size, destination_path, files)
process_photo(photo, file_size, destination_path, files, folder_fmt)
else:
LOGGER.debug(f"Skipping the unwanted photo {photo.filename}.")
for subalbum in album.subalbums:
Expand All @@ -120,6 +131,7 @@ def sync_album(album, destination_path, file_sizes, extensions=None, files=None)
file_sizes,
extensions,
files,
folder_fmt
)
return True

Expand All @@ -145,6 +157,7 @@ def sync_photos(config, photos):
filters = config_parser.get_photos_filters(config=config)
files = set()
download_all = config_parser.get_photos_all_albums(config=config)
folder_fmt = config_parser.get_folder_format(config=config)
if download_all:
for album in photos.albums.keys():
sync_album(
Expand All @@ -153,6 +166,7 @@ def sync_photos(config, photos):
file_sizes=filters["file_sizes"],
extensions=filters["extensions"],
files=files,
folder_fmt=folder_fmt,
)
elif filters["albums"]:
for album in iter(filters["albums"]):
Expand All @@ -162,6 +176,7 @@ def sync_photos(config, photos):
file_sizes=filters["file_sizes"],
extensions=filters["extensions"],
files=files,
folder_fmt=folder_fmt,
)
else:
sync_album(
Expand All @@ -170,6 +185,7 @@ def sync_photos(config, photos):
file_sizes=filters["file_sizes"],
extensions=filters["extensions"],
files=files,
folder_fmt=folder_fmt,
)

if config_parser.get_photos_remove_obsolete(config=config):
Expand Down
11 changes: 11 additions & 0 deletions tests/test_config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,3 +417,14 @@ def test_get_all_albums_false(self):
config["photos"]["all_albums"] = False
self.assertFalse(config_parser.get_photos_all_albums(config=config))

def test_folder_fmt_empty(self):
"""Empty folder_format."""
config = read_config(config_path=tests.CONFIG_PATH)
self.assertIsNone(config_parser.get_folder_format(config=config))

def test_folder_fmt_set(self):
"""folder_format is set."""
config = read_config(config_path=tests.CONFIG_PATH)
config["photos"]["folder_format"] = "%Y/%m"
self.assertEqual(config_parser.get_folder_format(config=config), "%Y/%m")

61 changes: 60 additions & 1 deletion tests/test_sync_photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def test_sync_photos_original(
)
@patch("icloudpy.ICloudPyService")
@patch("src.read_config")
def test_sync_photos_all_albums(
def test_sync_photos_all_albums_filtered(
self, mock_read_config, mock_service, mock_get_username, mock_get_password
):
"""Test for successful original photo size download."""
Expand All @@ -84,11 +84,68 @@ def test_sync_photos_all_albums(
album_1_path = os.path.join(
self.destination_path, config["photos"]["filters"]["albums"][1]
)
self.assertFalse(os.path.isdir(album_0_path))
self.assertFalse(os.path.isdir(album_1_path))

@patch(target="keyring.get_password", return_value=data.VALID_PASSWORD)
@patch(
target="src.config_parser.get_username", return_value=data.AUTHENTICATED_USER
)
@patch("icloudpy.ICloudPyService")
@patch("src.read_config")
def test_sync_photos_all_albums_not_filtered(
self, mock_read_config, mock_service, mock_get_username, mock_get_password
):
"""Test for successful original photo size download."""
mock_service = self.service
config = self.config.copy()
config["photos"]["destination"] = self.destination_path
config["photos"]["all_albums"] = True
mock_read_config.return_value = config
album_0_path = os.path.join(
self.destination_path, config["photos"]["filters"]["albums"][0]
)
album_1_path = os.path.join(
self.destination_path, config["photos"]["filters"]["albums"][1]
)
config["photos"]["filters"]["albums"] = None
# Sync original photos
self.assertIsNone(
sync_photos.sync_photos(config=config, photos=mock_service.photos)
)
self.assertTrue(os.path.isdir(album_0_path))
self.assertTrue(os.path.isdir(album_1_path))
self.assertTrue(len(os.listdir(album_0_path)) > 1)
self.assertTrue(len(os.listdir(album_1_path)) > 0)

@patch(target="keyring.get_password", return_value=data.VALID_PASSWORD)
@patch(
target="src.config_parser.get_username", return_value=data.AUTHENTICATED_USER
)
@patch("icloudpy.ICloudPyService")
@patch("src.read_config")
def test_sync_photos_folder_format(
self, mock_read_config, mock_service, mock_get_username, mock_get_password
):
"""Test for successful original photo size download with folder format."""
mock_service = self.service
config = self.config.copy()
config["photos"]["destination"] = self.destination_path
config["photos"]["folder_format"] = "%Y/%m"
mock_read_config.return_value = config
# Sync original photos
self.assertIsNone(
sync_photos.sync_photos(config=config, photos=mock_service.photos)
)
album_0_path = os.path.join(
self.destination_path, config["photos"]["filters"]["albums"][0]
)
album_1_path = os.path.join(
self.destination_path, config["photos"]["filters"]["albums"][1]
)
self.assertTrue(os.path.isdir(os.path.join(album_0_path, "2020", "08")))
self.assertTrue(os.path.isdir(os.path.join(album_1_path, "2020", "07")))

@patch(target="keyring.get_password", return_value=data.VALID_PASSWORD)
@patch(
target="src.config_parser.get_username", return_value=data.AUTHENTICATED_USER
Expand Down Expand Up @@ -491,6 +548,7 @@ def versions(self):
file_size="medium",
destination_path=self.destination_path,
files=None,
folder_fmt=None,
)
)

Expand All @@ -516,6 +574,7 @@ def versions(self):
file_size="thumb",
destination_path=self.destination_path,
files=None,
folder_fmt=None,
)
)

Expand Down

0 comments on commit aa4dddf

Please sign in to comment.