Add decollide_names, prefer simplified names in album exports.
This commit is contained in:
		
							parent
							
								
									28aa47d40d
								
							
						
					
					
						commit
						c7eda36133
					
				
					 1 changed files with 116 additions and 25 deletions
				
			
		|  | @ -19,7 +19,13 @@ from voussoirkit import pathclass | ||||||
| from . import constants | from . import constants | ||||||
| from . import exceptions | from . import exceptions | ||||||
| 
 | 
 | ||||||
| def album_as_directory_map(album, once_each=True, recursive=True): | def album_as_directory_map( | ||||||
|  |         album, | ||||||
|  |         naming='simplified', | ||||||
|  |         once_each=True, | ||||||
|  |         recursive=True, | ||||||
|  |         root_name=None, | ||||||
|  |     ): | ||||||
|     ''' |     ''' | ||||||
|     Given an album, produce a dictionary mapping Album objects to directory |     Given an album, produce a dictionary mapping Album objects to directory | ||||||
|     names as they will appear inside the zip archive. |     names as they will appear inside the zip archive. | ||||||
|  | @ -29,36 +35,61 @@ def album_as_directory_map(album, once_each=True, recursive=True): | ||||||
|         If an album is a child of multiple albums, only one instance is used. |         If an album is a child of multiple albums, only one instance is used. | ||||||
|     ''' |     ''' | ||||||
|     directories = {} |     directories = {} | ||||||
|     if album.title: |     if root_name is not None: | ||||||
|         title = remove_path_badchars(album.title) |         pass | ||||||
|         root_folder = f'album {album.id} - {title}' |     elif naming == 'simplified': | ||||||
|  |         root_name = album.display_name | ||||||
|  |     elif naming == 'unambiguous': | ||||||
|  |         root_name = album.full_name | ||||||
|     else: |     else: | ||||||
|         root_folder = f'album {album.id}' |         raise ValueError(naming) | ||||||
|  |     root_name = remove_path_badchars(root_name) | ||||||
| 
 | 
 | ||||||
|     if once_each: |     if once_each: | ||||||
|         directories[album] = root_folder |         directories[album] = root_name | ||||||
|     else: |     else: | ||||||
|         directories[album] = [root_folder] |         directories[album] = [root_name] | ||||||
| 
 | 
 | ||||||
|     if not recursive: |     if not recursive: | ||||||
|         return directories |         return directories | ||||||
| 
 | 
 | ||||||
|  |     children = album.get_children() | ||||||
|  |     if naming == 'simplified': | ||||||
|  |         child_names = decollide_names(children, lambda c: c.display_name) | ||||||
|  |     elif naming == 'unambiguous': | ||||||
|  |         child_names = {child: child.full_name for child in children} | ||||||
|  | 
 | ||||||
|  |     child_maps = ( | ||||||
|  |         album_as_directory_map( | ||||||
|  |             child, | ||||||
|  |             once_each=once_each, | ||||||
|  |             recursive=True, | ||||||
|  |             root_name=child_names[child], | ||||||
|  |         ) | ||||||
|  |         for child in children | ||||||
|  |     ) | ||||||
|     descendants = ( |     descendants = ( | ||||||
|         pair |         pair | ||||||
|         for child in album.get_children() |         for child_map in child_maps | ||||||
|         for pair in album_as_directory_map(child, once_each=once_each, recursive=True).items() |         for pair in child_map.items() | ||||||
|     ) |     ) | ||||||
|     for (child_album, child_directory) in descendants: |     for (child_album, child_directory) in descendants: | ||||||
|         if once_each: |         if once_each: | ||||||
|             child_directory = os.path.join(root_folder, child_directory) |             child_directory = os.path.join(root_name, child_directory) | ||||||
|             directories[child_album] = child_directory |             directories[child_album] = child_directory | ||||||
|         else: |         else: | ||||||
|             child_directory = [os.path.join(root_folder, d) for d in child_directory] |             child_directory = [os.path.join(root_name, d) for d in child_directory] | ||||||
|             directories.setdefault(child_album, []).extend(child_directory) |             directories.setdefault(child_album, []).extend(child_directory) | ||||||
| 
 | 
 | ||||||
|     return directories |     return directories | ||||||
| 
 | 
 | ||||||
| def album_photos_as_filename_map(album, once_each=True, recursive=True): | def album_photos_as_filename_map( | ||||||
|  |         album, | ||||||
|  |         naming='simplified', | ||||||
|  |         once_each=True, | ||||||
|  |         recursive=True, | ||||||
|  |         root_name=None, | ||||||
|  |     ): | ||||||
|     ''' |     ''' | ||||||
|     Given an album, produce a dictionary mapping Photo objects to the |     Given an album, produce a dictionary mapping Photo objects to the | ||||||
|     filenames that will appear inside the zip archive. |     filenames that will appear inside the zip archive. | ||||||
|  | @ -69,14 +100,21 @@ def album_photos_as_filename_map(album, once_each=True, recursive=True): | ||||||
|     ''' |     ''' | ||||||
|     arcnames = {} |     arcnames = {} | ||||||
| 
 | 
 | ||||||
|     directories = album_as_directory_map(album, once_each=once_each, recursive=recursive) |     directories = album_as_directory_map( | ||||||
|     photos = ( |         album, | ||||||
|         (photo, directory) |         once_each=once_each, | ||||||
|         for (album, directory) in directories.items() |         recursive=recursive, | ||||||
|         for photo in album.get_photos() |         root_name=root_name, | ||||||
|     ) |     ) | ||||||
|     for (photo, directory) in photos: | 
 | ||||||
|         photo_name = f'{photo.id} - {photo.basename}' |     for (album, directory) in directories.items(): | ||||||
|  |         photos = album.get_photos() | ||||||
|  |         if naming == 'simplified': | ||||||
|  |             photo_names = decollide_names(photos, lambda p: p.basename) | ||||||
|  |         elif naming == 'unambiguous': | ||||||
|  |             photo_names = {photo: f'{photo.id} - {photo.basename}' for photo in photos} | ||||||
|  |         for photo in photos: | ||||||
|  |             photo_name = photo_names[photo] | ||||||
|             if once_each: |             if once_each: | ||||||
|                 arcname = os.path.join(directory, photo_name) |                 arcname = os.path.join(directory, photo_name) | ||||||
|                 arcnames[photo] = arcname |                 arcnames[photo] = arcname | ||||||
|  | @ -144,6 +182,59 @@ def comma_space_split(s): | ||||||
|         return s |         return s | ||||||
|     return re.split(r'[ ,]+', s.strip()) |     return re.split(r'[ ,]+', s.strip()) | ||||||
| 
 | 
 | ||||||
|  | def decollide_names(things, namer): | ||||||
|  |     ''' | ||||||
|  |     When generating zip files, or otherwise exporting photos to disk, it is | ||||||
|  |     aesthetically preferable to export them using just their basename. But, | ||||||
|  |     since multiple photos might have the same basename, we occasionally need to | ||||||
|  |     use their IDs to disambiguate them. | ||||||
|  |     This function automates that by keeping the basename wherever possible, and | ||||||
|  |     prefixing items with their ID in the case of a name collision. | ||||||
|  |     This function takes `things`, which is a collection of either Albums or | ||||||
|  |     Photos, and `namer` which is a callable that gives us the preferred name | ||||||
|  |     of the thing (in practice, just a lambda returning Album title, | ||||||
|  |     Photo basename), and returns a map of {thing: name}. If there are duplicate | ||||||
|  |     names, they will be disambiguated by adding "id - " to the front. | ||||||
|  |     ''' | ||||||
|  |     # The majority of this algorithm is dedicated to solving the case where some | ||||||
|  |     # prankster has named their album such that it contains the ID of another | ||||||
|  |     # album. | ||||||
|  |     # For example, consider three Albums (1, "A"), (2, "A"), (3, "1 - A"). | ||||||
|  |     # So when 1 and 2 get disambiguated to (1, "1 - A"), (2, "2 - A"), | ||||||
|  |     # then suddenly there is a new collision between (1, "1 - A") and | ||||||
|  |     # (3, "1 - A"), and we need to disambiguate by renaming 3 to "3 - 1 - A". | ||||||
|  |     # I'm not totally happy with how this function looks, but as long as I get | ||||||
|  |     # it working I'll just stop looking at it and problem solved! | ||||||
|  |     collisions = {} | ||||||
|  |     final = {} | ||||||
|  |     for thing in things: | ||||||
|  |         name = namer(thing) | ||||||
|  |         collisions.setdefault(name, []).append(thing) | ||||||
|  |         final[thing] = name | ||||||
|  | 
 | ||||||
|  |     # When the thing is disambiguated by adding its ID, it's done being | ||||||
|  |     # decollided and can be locked. This ensures that if disambiguating one | ||||||
|  |     # thing causes a new collision with a prank entry, only the prank needs to | ||||||
|  |     # get renamed on the second pass. We don't need to keep prefixing the | ||||||
|  |     # thing's ID onto the same thing over and over again. | ||||||
|  |     locked = set() | ||||||
|  |     while True: | ||||||
|  |         collision = { | ||||||
|  |             name: set(things).difference(locked) | ||||||
|  |             for (name, things) in collisions.items() | ||||||
|  |             if len(things) > 1 | ||||||
|  |         } | ||||||
|  |         if not collision: | ||||||
|  |             break | ||||||
|  |         for (name, things) in collision.items(): | ||||||
|  |             for thing in things: | ||||||
|  |                 myname = f'{thing.id} - {name}' | ||||||
|  |                 locked.add(thing) | ||||||
|  |                 collisions[name].remove(thing) | ||||||
|  |                 collisions.setdefault(myname, []).append(thing) | ||||||
|  |                 final[thing] = myname | ||||||
|  |     return final | ||||||
|  | 
 | ||||||
| def dict_to_tuple(d): | def dict_to_tuple(d): | ||||||
|     return tuple(sorted(d.items())) |     return tuple(sorted(d.items())) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue