Skip to content

Commit 96d83bb

Browse files
authored
Merge pull request #45 from Salvoxia/feat-pathFilter
Feature: Path Filter
2 parents 901463d + 0a9691d commit 96d83bb

File tree

3 files changed

+135
-17
lines changed

3 files changed

+135
-17
lines changed

README.md

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ This script is mostly based on the following original script: [REDVM/immich_auto
1919
2. [Usage (Docker)](#docker)
2020
3. [Choosing the correct `root_path`](#choosing-the-correct-root_path)
2121
4. [How It Works (with Examples)](#how-it-works)
22-
5. [Automatic Album Sharing](#automatic-album-sharing)
23-
6. [Cleaning Up Albums](#cleaning-up-albums)
24-
7. [Assets in Multiple Albums](#assets-in-multiple-albums)
25-
8. [Dealing with External Library Changes](#dealing-with-external-library-changes)
22+
5. [Filtering](#filtering)
23+
6. [Automatic Album Sharing](#automatic-album-sharing)
24+
7. [Cleaning Up Albums](#cleaning-up-albums)
25+
8. [Assets in Multiple Albums](#assets-in-multiple-albums)
26+
9. [Dealing with External Library Changes](#dealing-with-external-library-changes)
2627

2728
## Usage
2829
### Bare Python Script
@@ -37,8 +38,8 @@ This script is mostly based on the following original script: [REDVM/immich_auto
3738
```
3839
3. Run the script
3940
```
40-
usage: immich_auto_album.py [-h] [-r ROOT_PATH] [-u] [-a ALBUM_LEVELS] [-s ALBUM_SEPARATOR] [-c CHUNK_SIZE] [-C FETCH_CHUNK_SIZE] [-l {CRITICAL,ERROR,WARNING,INFO,DEBUG}] [-k] [-i IGNORE] [-m {CREATE,CLEANUP,DELETE_ALL}] [-d] [-x SHARE_WITH] [-o {viewer,editor}]
41-
[-S {0,1,2}] [-O {False,asc,desc}] [-A]
41+
usage: immich_auto_album.py [-h] [-r ROOT_PATH] [-u] [-a ALBUM_LEVELS] [-s ALBUM_SEPARATOR] [-c CHUNK_SIZE] [-C FETCH_CHUNK_SIZE] [-l {CRITICAL,ERROR,WARNING,INFO,DEBUG}] [-k] [-i IGNORE] [-m {CREATE,CLEANUP,DELETE_ALL}] [-d]
42+
[-x SHARE_WITH] [-o {viewer,editor}] [-S {0,1,2}] [-O {False,asc,desc}] [-A] [-f PATH_FILTER]
4243
root_path api_url api_key
4344

4445
Create Immich Albums from an external library path based on the top level folders
@@ -54,8 +55,9 @@ This script is mostly based on the following original script: [REDVM/immich_auto
5455
Additional external libarary root path in Immich; May be specified multiple times for multiple import paths or external libraries. (default: None)
5556
-u, --unattended Do not ask for user confirmation after identifying albums. Set this flag to run script as a cronjob. (default: False)
5657
-a ALBUM_LEVELS, --album-levels ALBUM_LEVELS
57-
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
58-
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>. (default: 1)
58+
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
59+
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
60+
<endLevel>. (default: 1)
5961
-s ALBUM_SEPARATOR, --album-separator ALBUM_SEPARATOR
6062
Separator string to use for compound album names created from nested folders. Only effective if -a is set to a value > 1 (default: )
6163
-c CHUNK_SIZE, --chunk-size CHUNK_SIZE
@@ -68,21 +70,27 @@ This script is mostly based on the following original script: [REDVM/immich_auto
6870
-i IGNORE, --ignore IGNORE
6971
A string containing a list of folders, sub-folder sequences or file names separated by ':' that will be ignored. (default: )
7072
-m {CREATE,CLEANUP,DELETE_ALL}, --mode {CREATE,CLEANUP,DELETE_ALL}
71-
Mode for the script to run with. CREATE = Create albums based on folder names and provided arguments; CLEANUP = Create album nmaes based on current images and script arguments, but delete albums if they exist; DELETE_ALL = Delete all
72-
albums. If the mode is anything but CREATE, --unattended does not have any effect. Only performs deletion if -d/--delete-confirm option is set, otherwise only performs a dry-run. (default: CREATE)
73+
Mode for the script to run with. CREATE = Create albums based on folder names and provided arguments; CLEANUP = Create album nmaes based on current images and script arguments, but delete albums if they
74+
exist; DELETE_ALL = Delete all albums. If the mode is anything but CREATE, --unattended does not have any effect. Only performs deletion if -d/--delete-confirm option is set, otherwise only performs a
75+
dry-run. (default: CREATE)
7376
-d, --delete-confirm Confirm deletion of albums when running in mode CLEANUP or DELETE_ALL. If this flag is not set, these modes will perform a dry run only. Has no effect in mode CREATE (default: False)
7477
-x SHARE_WITH, --share-with SHARE_WITH
75-
A user name (or email address of an existing user) to share newly created albums with. Sharing only happens if the album was actually created, not if new assets were added to an existing album. If the the share role should be specified by
76-
user, the format <userName>=<shareRole> must be used, where <shareRole> must be one of 'viewer' or 'editor'. May be specified multiple times to share albums with more than one user. (default: None)
78+
A user name (or email address of an existing user) to share newly created albums with. Sharing only happens if the album was actually created, not if new assets were added to an existing album. If the
79+
the share role should be specified by user, the format <userName>=<shareRole> must be used, where <shareRole> must be one of 'viewer' or 'editor'. May be specified multiple times to share albums with
80+
more than one user. (default: None)
7781
-o {viewer,editor}, --share-role {viewer,editor}
78-
The default share role for users newly created albums are shared with. Only effective if --share-with is specified at least once and the share role is not specified within --share-with. (default: viewer)
82+
The default share role for users newly created albums are shared with. Only effective if --share-with is specified at least once and the share role is not specified within --share-with. (default:
83+
viewer)
7984
-S {0,1,2}, --sync-mode {0,1,2}
80-
Synchronization mode to use. Synchronization mode helps synchronizing changes in external libraries structures to Immich after albums have already been created. Possible Modes: 0 = do nothing; 1 = Delete any empty albums; 2 = Trigger
81-
offline asset removal (REQUIRES API KEY OF AN ADMIN USER!) (default: 0)
85+
Synchronization mode to use. Synchronization mode helps synchronizing changes in external libraries structures to Immich after albums have already been created. Possible Modes: 0 = do nothing; 1 =
86+
Delete any empty albums; 2 = Trigger offline asset removal (REQUIRES API KEY OF AN ADMIN USER!) (default: 0)
8287
-O {False,asc,desc}, --album-order {False,asc,desc}
8388
Set sorting order for newly created albums to newest or oldest file first, Immich defaults to newest file first (default: False)
8489
-A, --find-assets-in-albums
85-
By default, the script only finds assets that are not assigned to any album yet. Set this option to make the script discover assets that are already part of an album and handle them as usual. (default: False)
90+
By default, the script only finds assets that are not assigned to any album yet. Set this option to make the script discover assets that are already part of an album and handle them as usual. (default:
91+
False)
92+
-f PATH_FILTER, --path-filter PATH_FILTER
93+
Use glob-like patterns to filter assets before album name creation. This filter is evaluated before any values passed with --ignore. (default: )
8694
```
8795
8896
__Plain example without optional arguments:__
@@ -117,6 +125,7 @@ The environment variables are analoguous to the script's command line arguments.
117125
| SYNC_MODE | no | Synchronization mode to use. Synchronization mode helps synchronizing changes in external libraries structures to Immich after albums have already been created. Possible Modes: <br>`0` = do nothing<br>`1` = Delete any empty albums<br>`2` = Trigger offline asset removal (REQUIRES API KEY OF AN ADMIN USER!)<br>(default: `0`)<br>Refer to [Dealing with External Library Changes](#dealing-with-external-library-changes). |
118126
| ALBUM_ORDER | no | Set sorting order for newly created albums to newest (`desc`) or oldest (`asc`) file first, Immich defaults to newest file first, allowed values: `asc`, `desc` |
119127
| FIND_ASSETS_IN_ALBUMS | no | By default, the script only finds assets that are not assigned to any album yet. Set this option to make the script discover assets that are already part of an album and handle them as usual. (default: `False`)<br>Refer to [Assets in Multiple Albums](#assets-in-multiple-albums). |
128+
| PATH_FILTER | no | Use glob-like patterns to filter assets before album name creation. This filter is evaluated before any values passed with --ignore. (default: ``)<br>Refer to [Filtering](#filtering). |
120129

121130
#### Run the container with Docker
122131

@@ -267,6 +276,62 @@ Albums created for `root_path = /external_libs/photos/Birthdays`:
267276
268277
Since Immich does not support real nested albums ([yet?](https://github.com/immich-app/immich/discussions/2073)), neither does this script.
269278
279+
## Filtering
280+
281+
It is possible filter images by either specifying path patterns to include or keywords which will ignore an image if its path contains any. Two options control this behavior.
282+
283+
### Ignoring Assets
284+
The option `-i / --ignore` or Docker environment variable `IGNORE` accepts a semicolon-separated `:` list of keywords. If an image's path contains that keyword, it will be ignored.
285+
286+
**Example:**
287+
`--ignore "Vacation:Birthday"` will not include any images for which the path **below the root path** contains either `Vacation` or `Birthday`. Albums will not be created for these images and they will not be added to albums.
288+
289+
### Filtering for Assets
290+
The option `-f / ---path-filter` or Docker environment variable `PATH_FILTER` accepts a glob-style pattern to filter for images for which the path **below the root path** matches the provided pattern. **Only** these images will be considered for album creation.
291+
The following wild-cards are supported:
292+
| Pattern | Meaning |
293+
|---------|---------------------------------------------------------------------------------------------|
294+
|`*` | Matches everything (even nothing) within one folder level |
295+
|`?` | Matches any single character |
296+
|`[]` | Matches one character in the brackets, e.g. `[a]` literally matches `a` |
297+
|`[!]` | Matches one character *not* in the brackets, e.h. `[!a]` matches any character **but** `a` |
298+
299+
> [!TIP]
300+
> When working with path filters, consider setting the `-A / --find-assets-in-albums` option or Docker environment variable `FIND_ASSETS_IN_ALBUMS` for the script to discover assets that are already part of an album. That way, assets can be added to multiple albums by the script. Refer to the [Assets in Multiple Albums](#assets-in-multiple-albums) section for more information.
301+
302+
**Examples:**
303+
Consider the following folder structure:
304+
```
305+
/external_libs/photos/
306+
├── 2020/
307+
│ ├── 02 Feb/
308+
│ │ └── Vacation/
309+
│ ├── 08 Aug/
310+
│ │ └── Vacation/
311+
├── Birthdays/
312+
│ ├── John/
313+
│ └── Jane/
314+
└── Skiing 2023/
315+
```
316+
317+
- To only create a `Birthdays` album with all images directly in `Birthdays` or in any subfolder on any level, run the script with the following options:
318+
- `root_path=/external_libs/photos`
319+
- `--album-level=1`
320+
- `--path-filter Birthdays/**`
321+
- To only create albums for the 2020s (all 202x years), but with the album names like `2020 02 Feb`, run the script with the following options:
322+
- `root_path=/external_libs/photos`
323+
- `--album-level=2`
324+
- `--path-filter=202?/**`
325+
- To only create albums for 2020s (all 202x years) with the album names like `2020 02 Feb`, but only with images in folders **one level** below `2020` and **not** any of the `Vacation` images, run the script with the following options:
326+
- `root_path=/external_libs/photos`
327+
- `--album-level=2`
328+
- `--path-filter=202?/*/*`
329+
- To create a `Vacation` album with all vacation images, run the script with the following options:
330+
- `root_path=/external_libs/photos`
331+
- `--album-level=-1`
332+
- `--path-filter=**/Vacation/*`
333+
334+
270335
## Automatic Album Sharing
271336

272337
The scripts support sharing newly created albums with a list of existing users. The sharing role (`viewer` or `editor`) can be specified for all users at once or individually per user.
@@ -335,6 +400,8 @@ The script will generate album names using the script's arguments and the assets
335400

336401
By default, the script only fetches assets from Immich that are not assigned to any album yet. This makes querying assets in large libraries very fast. However, if assets should be part of either manually created albums as well as albums based on the folder structure, or if multiple script passes with different album level settings should create differently named albums with overlapping contents, the option `--find-assets-in-albums` (bare Python) or environment variable `FIND_ASSETS_IN_ALBUMS` (Docker) may be set.
337402
In that case, the script will request all assets from Immich and add them to their corresponding folders, even if the also are part of other albums.
403+
> [!TIP]
404+
> This option can be especially useful when [Filtering for Assets](#filtering-for-assets).
338405

339406

340407
## Dealing with External Library Changes

docker/immich_auto_album.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,9 @@ if [ ! -z "$FIND_ASSETS_IN_ALBUMS" ]; then
9595
args="-A $args"
9696
fi
9797

98+
if [ ! -z "$PATH_FILTER" ]; then
99+
args="-f \"$PATH_FILTER\" $args"
100+
fi
101+
98102
BASEDIR=$(dirname "$0")
99103
echo $args | xargs python3 -u $BASEDIR/immich_auto_album.py

immich_auto_album.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import sys
77
import os
88
import datetime
9-
from collections import defaultdict
9+
from collections import defaultdict, OrderedDict
10+
import re
1011
import urllib3
1112

13+
1214
# Trying to deal with python's isnumeric() function
1315
# not recognizing negative numbers
1416
def is_integer(str):
@@ -18,6 +20,30 @@ def is_integer(str):
1820
except ValueError:
1921
return False
2022

23+
# Translation of GLOB-style patterns to Regex
24+
# Source: https://stackoverflow.com/a/63212852
25+
# FIXME: Replace with glob.translate() introduced with Python 3.13
26+
escaped_glob_tokens_to_re = OrderedDict((
27+
# Order of ``**/`` and ``/**`` in RE tokenization pattern doesn't matter because ``**/`` will be caught first no matter what, making ``/**`` the only option later on.
28+
# W/o leading or trailing ``/`` two consecutive asterisks will be treated as literals.
29+
('/\\*\\*', '(?:/.+?)*'), # Edge-case #1. Catches recursive globs in the middle of path. Requires edge case #2 handled after this case.
30+
('\\*\\*/', '(?:^.+?/)*'), # Edge-case #2. Catches recursive globs at the start of path. Requires edge case #1 handled before this case. ``^`` is used to ensure proper location for ``**/``.
31+
('\\*', '[^/]*'), # ``[^/]*`` is used to ensure that ``*`` won't match subdirs, as with naive ``.*?`` solution.
32+
('\\?', '.'),
33+
('\\[\\*\\]', '\\*'), # Escaped special glob character.
34+
('\\[\\?\\]', '\\?'), # Escaped special glob character.
35+
('\\[!', '[^'), # Requires ordered dict, so that ``\\[!`` preceded ``\\[`` in RE pattern. Needed mostly to differentiate between ``!`` used within character class ``[]`` and outside of it, to avoid faulty conversion.
36+
('\\[', '['),
37+
('\\]', ']'),
38+
))
39+
40+
escaped_glob_replacement = re.compile('(%s)' % '|'.join(escaped_glob_tokens_to_re).replace('\\', '\\\\\\'))
41+
42+
def glob_to_re(pattern):
43+
return escaped_glob_replacement.sub(lambda match: escaped_glob_tokens_to_re[match.group(0)], re.escape(pattern))
44+
45+
46+
2147
# Constants holding script run modes
2248
# Creat albums based on folder names and script arguments
2349
SCRIPT_MODE_CREATE = "CREATE"
@@ -53,6 +79,7 @@ def is_integer(str):
5379
parser.add_argument("-S", "--sync-mode", default=0, type=int, choices=[0, 1, 2], help="Synchronization mode to use. Synchronization mode helps synchronizing changes in external libraries structures to Immich after albums have already been created. Possible Modes: 0 = do nothing; 1 = Delete any empty albums; 2 = Trigger offline asset removal (REQUIRES API KEY OF AN ADMIN USER!)")
5480
parser.add_argument("-O", "--album-order", default=False, type=str, choices=[False, 'asc', 'desc'], help="Set sorting order for newly created albums to newest or oldest file first, Immich defaults to newest file first")
5581
parser.add_argument("-A", "--find-assets-in-albums", action="store_true", help="By default, the script only finds assets that are not assigned to any album yet. Set this option to make the script discover assets that are already part of an album and handle them as usual.")
82+
parser.add_argument("-f", "--path-filter", default="", type=str, help="Use glob-like patterns to filter assets before album name creation. This filter is evaluated before any values passed with --ignore.")
5683

5784
args = vars(parser.parse_args())
5885
# set up logger to log in logfmt format
@@ -79,6 +106,7 @@ def is_integer(str):
79106
share_role = args["share_role"]
80107
sync_mode = args["sync_mode"]
81108
find_assets_in_albums = args["find_assets_in_albums"]
109+
path_filter = args["path_filter"]
82110

83111
# Override unattended if we're running in destructive mode
84112
if mode != SCRIPT_MODE_CREATE:
@@ -105,6 +133,7 @@ def is_integer(str):
105133
logging.debug("share_role = %s", share_role)
106134
logging.debug("sync_mode = %d", sync_mode)
107135
logging.debug("find_assets_in_albums = %s", find_assets_in_albums)
136+
logging.debug("path_filter = %s", path_filter)
108137

109138
# Verify album levels
110139
if is_integer(album_levels) and album_levels == 0:
@@ -150,6 +179,17 @@ def is_integer(str):
150179
else:
151180
ignore_albums = False
152181

182+
path_filter_regex = False
183+
if path_filter == "":
184+
path_filter = False
185+
else:
186+
# # Check if last porition of glob pattern contains a dot '.'
187+
# path_filter_parsed = path_filter.split('/')
188+
# if not '.' in path_filter_parsed[len(path_filter_parsed)-1]:
189+
# # Include all files
190+
# path_filter += "/*.*"
191+
path_filter_regex = glob_to_re(path_filter)
192+
153193
# Request arguments for API calls
154194
requests_kwargs = {
155195
'headers' : {
@@ -527,6 +567,7 @@ def triggerOfflineAssetRemoval(libraryId: str):
527567
assert r.status_code == 204
528568

529569

570+
530571
# append trailing slash to all root paths
531572
for i in range(len(root_paths)):
532573
if root_paths[i][-1] != '/':
@@ -584,6 +625,12 @@ def triggerOfflineAssetRemoval(libraryId: str):
584625
for root_path in root_paths:
585626
if root_path not in asset_path:
586627
continue
628+
629+
# First apply filter, if any
630+
if path_filter:
631+
if not re.fullmatch(path_filter_regex, asset_path.replace(root_path, '')):
632+
logging.debug("Ignoring asset %s due to path_filter setting!", asset_path)
633+
continue
587634
# Check ignore_albums
588635
ignore = False
589636
if ignore_albums:

0 commit comments

Comments
 (0)