Skip to content

Commit 2771cc3

Browse files
authored
Merge pull request #12 from Salvoxia/dev
Added support for album level ranges to support creating album names …
2 parents 945cbbc + 891ea9d commit 2771cc3

File tree

1 file changed

+95
-18
lines changed

1 file changed

+95
-18
lines changed

immich_auto_album.py

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,25 @@
44
import logging
55
import sys
66
import datetime
7+
import array as arr
78
from collections import defaultdict
89

10+
# Trying to deal with python's isnumeric() function
11+
# not recognizing negative numbers
12+
def is_integer(str):
13+
try:
14+
int(str)
15+
return True
16+
except ValueError:
17+
return False
918

1019
parser = argparse.ArgumentParser(description="Create Immich Albums from an external library path based on the top level folders", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
1120
parser.add_argument("root_path", action='append', help="The external libarary's root path in Immich")
1221
parser.add_argument("api_url", help="The root API URL of immich, e.g. https://immich.mydomain.com/api/")
1322
parser.add_argument("api_key", help="The Immich API Key to use")
1423
parser.add_argument("-r", "--root-path", action="append", help="Additional external libarary root path in Immich; May be specified multiple times for multiple import paths or external libraries.")
1524
parser.add_argument("-u", "--unattended", action="store_true", help="Do not ask for user confirmation after identifying albums. Set this flag to run script as a cronjob.")
16-
parser.add_argument("-a", "--album-levels", default=1, type=int, help="Number of sub-folders below the root path used for album name creation. Positive numbers start from top of the folder structure, negative numbers from the bottom. Cannot be 0.")
25+
parser.add_argument("-a", "--album-levels", default="1", type=str, help="Number of sub-folders or range of sub-folder levels below the root path used for album name creation. Positive numbers start from top of the folder structure, negative numbers from the bottom. Cannot be 0. If a range should be set, the start level and end level must be separated by a comma like '<startLevel>,<endLevel>'. If negative levels are used in a range, <startLevel> must be less than or equal to <endLevel>.")
1726
parser.add_argument("-s", "--album-separator", default=" ", type=str, help="Separator string to use for compound album names created from nested folders. Only effective if -a is set to a value > 1")
1827
parser.add_argument("-c", "--chunk-size", default=2000, type=int, help="Maximum number of assets to add to an album with a single API call")
1928
parser.add_argument("-C", "--fetch-chunk-size", default=5000, type=int, help="Maximum number of assets to fetch with a single API call")
@@ -30,21 +39,55 @@
3039
number_of_assets_to_fetch_per_request = args["fetch_chunk_size"]
3140
unattended = args["unattended"]
3241
album_levels = args["album_levels"]
42+
# Album Levels Range handling
43+
album_levels_range_arr = ()
3344
album_level_separator = args["album_separator"]
3445
logging.debug("root_path = %s", root_paths)
3546
logging.debug("root_url = %s", root_url)
3647
logging.debug("api_key = %s", api_key)
3748
logging.debug("number_of_images_per_request = %d", number_of_images_per_request)
3849
logging.debug("number_of_assets_to_fetch_per_request = %d", number_of_assets_to_fetch_per_request)
3950
logging.debug("unattended = %s", unattended)
40-
logging.debug("album_levels = %d", album_levels)
51+
logging.debug("album_levels = %s", album_levels)
52+
#logging.debug("album_levels_range = %s", album_levels_range)
4153
logging.debug("album_level_separator = %s", album_level_separator)
4254

4355
# Verify album levels
44-
if album_levels == 0:
56+
if is_integer(album_levels) and album_levels == 0:
4557
parser.print_help()
4658
exit(1)
4759

60+
# Verify album levels range
61+
if not is_integer(album_levels):
62+
album_levels_range_split = album_levels.split(",")
63+
if (len(album_levels_range_split) != 2
64+
or not is_integer(album_levels_range_split[0])
65+
or not is_integer(album_levels_range_split[1])
66+
or int(album_levels_range_split[0]) == 0
67+
or int(album_levels_range_split[1]) == 0
68+
or (int(album_levels_range_split[0]) >= 0 and int(album_levels_range_split[1]) < 0)
69+
or (int(album_levels_range_split[0]) < 0 and int(album_levels_range_split[1]) >= 0)
70+
or (int(album_levels_range_split[0]) < 0 and int(album_levels_range_split[1]) < 0) and int(album_levels_range_split[0]) > int(album_levels_range_split[1])):
71+
logging.error("Invalid album_levels range format! If a range should be set, the start level and end level must be separated by a comma like '<startLevel>,<endLevel>'. If negative levels are used in a range, <startLevel> must be less than or equal to <endLevel>.")
72+
exit(1)
73+
album_levels_range_arr = album_levels_range_split
74+
# Convert to int
75+
album_levels_range_arr[0] = int(album_levels_range_split[0])
76+
album_levels_range_arr[1] = int(album_levels_range_split[1])
77+
# Special case: both levels are negative and end level is -1, which is equivalent to just negative album level of start level
78+
if(album_levels_range_arr[0] < 0 and album_levels_range_arr[1] == -1):
79+
album_levels = album_levels_range_arr[0]
80+
album_levels_range_arr = ()
81+
logging.debug("album_levels is a range with negative start level and end level of -1, converted to album_levels = %d", album_levels)
82+
else:
83+
logging.debug("valid album_levels range argument supplied")
84+
logging.debug("album_levels_start_level = %d", album_levels_range_arr[0])
85+
logging.debug("album_levels_end_level = %d", album_levels_range_arr[1])
86+
# Deduct 1 from album start levels, since album levels start at 1 for user convenience, but arrays start at index 0
87+
if album_levels_range_arr[0] > 0:
88+
album_levels_range_arr[0] -= 1
89+
album_levels_range_arr[1] -= 1
90+
4891
# Yield successive n-sized
4992
# chunks from l.
5093
def divide_chunks(l, n):
@@ -53,6 +96,50 @@ def divide_chunks(l, n):
5396
for i in range(0, len(l), n):
5497
yield l[i:i + n]
5598

99+
# Create album names from provided path_chunks string array
100+
# based on supplied album_levels argument (either by level range or absolute album levels)
101+
def create_album_name(path_chunks):
102+
album_name_chunks = ()
103+
logging.debug("path chunks = %s", list(path_chunks))
104+
# Check which path to take: album_levels_range or album_levels
105+
if len(album_levels_range_arr) == 2:
106+
if album_levels_range_arr[0] < 0:
107+
album_levels_start_level_capped = min(len(path_chunks), abs(album_levels_range_arr[0]))
108+
album_levels_end_level_capped = album_levels_range_arr[1]+1
109+
album_levels_start_level_capped *= -1
110+
else:
111+
album_levels_start_level_capped = min(len(path_chunks)-1, album_levels_range_arr[0])
112+
# Add 1 to album_levels_end_level_capped to include the end index, which is what the user intended to. It's not a problem
113+
# if the end index is out of bounds.
114+
album_levels_end_level_capped = min(len(path_chunks)-1, album_levels_range_arr[1]) + 1
115+
logging.debug("album_levels_start_level_capped = %d", album_levels_start_level_capped)
116+
logging.debug("album_levels_end_level_capped = %d", album_levels_end_level_capped)
117+
# album start level is not equal to album end level, so we want a range of levels
118+
if album_levels_start_level_capped is not album_levels_end_level_capped:
119+
120+
# if the end index is out of bounds.
121+
if album_levels_end_level_capped < 0 and abs(album_levels_end_level_capped) >= len(path_chunks):
122+
album_name_chunks = path_chunks[album_levels_start_level_capped:]
123+
else:
124+
album_name_chunks = path_chunks[album_levels_start_level_capped:album_levels_end_level_capped]
125+
# album start and end levels are equal, we want exactly that level
126+
else:
127+
# create on-the-fly array with a single element taken from
128+
album_name_chunks = [path_chunks[album_levels_start_level_capped]]
129+
else:
130+
album_levels_int = int(album_levels)
131+
# either use as many path chunks as we have,
132+
# or the specified album levels
133+
album_name_chunk_size = min(len(path_chunks), abs(album_levels_int))
134+
if album_levels_int < 0:
135+
album_name_chunk_size *= -1
136+
137+
# Copy album name chunks from the path to use as album name
138+
album_name_chunks = path_chunks[:album_name_chunk_size]
139+
if album_name_chunk_size < 0:
140+
album_name_chunks = path_chunks[album_name_chunk_size:]
141+
logging.debug("album_name_chunks = %s", album_name_chunks)
142+
return album_level_separator.join(album_name_chunks)
56143

57144
requests_kwargs = {
58145
'headers' : {
@@ -108,21 +195,11 @@ def divide_chunks(l, n):
108195

109196
# remove last item from path chunks, which is the file name
110197
del path_chunks[-1]
111-
album_name_chunks = ()
112-
# either use as many path chunks as we have,
113-
# or the specified album levels
114-
album_name_chunk_size = min(len(path_chunks), album_levels)
115-
if album_levels < 0:
116-
album_name_chunk_size = min(len(path_chunks), abs(album_levels))*-1
117-
118-
# Copy album name chunks from the path to use as album name
119-
album_name_chunks = path_chunks[:album_name_chunk_size]
120-
if album_name_chunk_size < 0:
121-
album_name_chunks = path_chunks[album_name_chunk_size:]
122-
123-
album_name = album_level_separator.join(album_name_chunks)
124-
# Check that the extracted album name is not actually a file name in root_path
125-
album_to_assets[album_name].append(asset['id'])
198+
album_name = create_album_name(path_chunks)
199+
if len(album_name) > 0:
200+
album_to_assets[album_name].append(asset['id'])
201+
else:
202+
logging.warning("Got empty album name for asset path %s, check your album_level settings!", asset_path)
126203

127204
album_to_assets = {k:v for k, v in sorted(album_to_assets.items(), key=(lambda item: item[0]))}
128205

0 commit comments

Comments
 (0)