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 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
|
||||
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.
|
||||
'''
|
||||
directories = {}
|
||||
if album.title:
|
||||
title = remove_path_badchars(album.title)
|
||||
root_folder = f'album {album.id} - {title}'
|
||||
if root_name is not None:
|
||||
pass
|
||||
elif naming == 'simplified':
|
||||
root_name = album.display_name
|
||||
elif naming == 'unambiguous':
|
||||
root_name = album.full_name
|
||||
else:
|
||||
root_folder = f'album {album.id}'
|
||||
raise ValueError(naming)
|
||||
root_name = remove_path_badchars(root_name)
|
||||
|
||||
if once_each:
|
||||
directories[album] = root_folder
|
||||
directories[album] = root_name
|
||||
else:
|
||||
directories[album] = [root_folder]
|
||||
directories[album] = [root_name]
|
||||
|
||||
if not recursive:
|
||||
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 = (
|
||||
pair
|
||||
for child in album.get_children()
|
||||
for pair in album_as_directory_map(child, once_each=once_each, recursive=True).items()
|
||||
for child_map in child_maps
|
||||
for pair in child_map.items()
|
||||
)
|
||||
for (child_album, child_directory) in descendants:
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
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 = {}
|
||||
|
||||
directories = album_as_directory_map(album, once_each=once_each, recursive=recursive)
|
||||
photos = (
|
||||
(photo, directory)
|
||||
for (album, directory) in directories.items()
|
||||
for photo in album.get_photos()
|
||||
directories = album_as_directory_map(
|
||||
album,
|
||||
once_each=once_each,
|
||||
recursive=recursive,
|
||||
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:
|
||||
arcname = os.path.join(directory, photo_name)
|
||||
arcnames[photo] = arcname
|
||||
|
@ -144,6 +182,59 @@ def comma_space_split(s):
|
|||
return s
|
||||
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):
|
||||
return tuple(sorted(d.items()))
|
||||
|
||||
|
|
Loading…
Reference in a new issue