diff --git a/README.md b/README.md index 3fdebfd61..de5f687e7 100644 --- a/README.md +++ b/README.md @@ -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 + unique_filenames: false # Optional, default false. If true all files have globally unique filenames, it should matter when filter > albums is empty and one photo is in multiple album 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 diff --git a/src/config_parser.py b/src/config_parser.py index 58a58f707..53392f62d 100644 --- a/src/config_parser.py +++ b/src/config_parser.py @@ -106,6 +106,15 @@ def get_photos_all_albums(config): return download_all +def get_photos_unique_file_names(config): + """Return flag to use unique filenames.""" + unique_filenames = False + config_path = ["photos", "unique_filenames"] + if traverse_config_path(config=config, config_path=config_path): + unique_filenames = get_config_value(config=config, config_path=config_path) + LOGGER.info("Using unique filenames.") + return unique_filenames + def prepare_root_destination(config): """Prepare root destination.""" LOGGER.debug("Checking root destination ...") diff --git a/src/sync_photos.py b/src/sync_photos.py index fbb864e75..7a274e051 100644 --- a/src/sync_photos.py +++ b/src/sync_photos.py @@ -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, unique_file_names): """Generate full path to file.""" filename = photo.filename name, extension = filename.rsplit(".", 1) if "." in filename else [filename, ""] @@ -48,7 +48,28 @@ def generate_file_name(photo, file_size, destination_path): os.rename(file_size_path, file_size_id_path) if os.path.isfile(file_size_id_path): os.rename(file_size_id_path, file_size_id_path_norm) - return file_size_id_path_norm + + photo_file_name = file_size_id_path_norm + + if unique_file_names: + album_name = destination_path.split("/")[-1] + file_size_id_album_name = [ + album_name, + name, + file_size, + base64.urlsafe_b64encode(photo.id.encode()).decode()[2:10], + ] + file_size_id_album_name_short_path = os.path.join( + destination_path, + f'{"__".join(file_size_id_album_name)}.{extension}', + ) + photo_file_name = unicodedata.normalize( + "NFC", file_size_id_album_name_short_path + ) + if os.path.isfile(file_size_id_path_norm): + os.rename(file_size_id_path_norm, photo_file_name) + + return photo_file_name def photo_exists(photo, file_size, local_path): @@ -83,10 +104,13 @@ 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, unique_file_names): """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, + unique_file_names=unique_file_names, ) if file_size not in photo.versions: LOGGER.warning( @@ -101,7 +125,14 @@ 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, + unique_file_names=False, +): """Sync given album.""" if album is None or destination_path is None or file_sizes is None: return None @@ -110,7 +141,9 @@ 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, unique_file_names + ) else: LOGGER.debug(f"Skipping the unwanted photo {photo.filename}.") for subalbum in album.subalbums: @@ -120,6 +153,7 @@ def sync_album(album, destination_path, file_sizes, extensions=None, files=None) file_sizes, extensions, files, + unique_file_names=unique_file_names, ) return True @@ -145,6 +179,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) + unique_file_names = config_parser.get_photos_unique_file_names(config=config) if download_all: for album in photos.albums.keys(): sync_album( @@ -153,6 +188,7 @@ def sync_photos(config, photos): file_sizes=filters["file_sizes"], extensions=filters["extensions"], files=files, + unique_file_names=unique_file_names, ) elif filters["albums"]: for album in iter(filters["albums"]): @@ -162,6 +198,7 @@ def sync_photos(config, photos): file_sizes=filters["file_sizes"], extensions=filters["extensions"], files=files, + unique_file_names=unique_file_names, ) else: sync_album( @@ -170,6 +207,7 @@ def sync_photos(config, photos): file_sizes=filters["file_sizes"], extensions=filters["extensions"], files=files, + unique_file_names=unique_file_names, ) if config_parser.get_photos_remove_obsolete(config=config): diff --git a/tests/test_sync_photos.py b/tests/test_sync_photos.py index d1821d887..aaf146f2a 100644 --- a/tests/test_sync_photos.py +++ b/tests/test_sync_photos.py @@ -491,6 +491,7 @@ def versions(self): file_size="medium", destination_path=self.destination_path, files=None, + unique_file_names=False, ) ) @@ -516,6 +517,7 @@ def versions(self): file_size="thumb", destination_path=self.destination_path, files=None, + unique_file_names=False, ) )