Skip to content

Instantly share code, notes, and snippets.

@REDVM
Last active April 17, 2024 09:57
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save REDVM/d8b3830b2802db881f5b59033cf35702 to your computer and use it in GitHub Desktop.
Save REDVM/d8b3830b2802db881f5b59033cf35702 to your computer and use it in GitHub Desktop.
Create and populate albums on Immich based on folder name
import requests
import os
from collections import defaultdict
# I have photos in subfolders like :
# /mnt/media/Photos/2023-08 Holidays
# /mnt/media/Photos/2023-06 Birthday
# /mnt/media/Photos/2022-12 Christmas
# This script will create 3 albums
# 2023-08 Holidays, 2023-06 Birthday, 2022-12 Christmas
# And populate them with the photos inside
# The script can be run multiple times to update, new albums will be created,
# or new photos added in existing subfolder will be added to corresponding album
# Personnalize here
root_path = '/mnt/media/Photos/'
root_url = 'http://127.0.0.1:2283/api/'
api_key = 'Your-api-key-here'
requests_kwargs = {
'headers' : {
'x-api-key': api_key,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}
if root_path[-1] != '/':
root_path = root_path + '/'
if root_url[-1] != '/':
root_url = root_url + '/'
print(" 1. Requesting all assets")
r = requests.get(root_url+'asset', **requests_kwargs)
assert r.status_code == 200
assets = r.json()
print(len(assets), "photos found")
print(" 2. Sorting assets to corresponding albums using folder name")
album_to_assets = defaultdict(list)
for asset in assets:
asset_path = asset['originalPath']
if root_path not in asset_path:
continue
album_name = asset_path.replace(root_path, '').split('/')[0]
album_to_assets[album_name].append(asset['id'])
album_to_assets = {k:v for k, v in sorted(album_to_assets.items(), key=(lambda item: item[0]))}
print(len(album_to_assets), "albums identified")
print(list(album_to_assets.keys()))
print("Press Enter to continue, Ctrl+C to abort")
input()
album_to_id = {}
print(" 3. Listing existing albums on immich")
r = requests.get(root_url+'album', **requests_kwargs)
assert r.status_code == 200
albums = r.json()
album_to_id = {album['albumName']:album['id'] for album in albums }
print(len(albums), "existing albums identified")
print(" 4. Creating albums if needed")
cpt = 0
for album in album_to_assets:
if album in album_to_id:
continue
data = {
'albumName': album,
'description': album
}
r = requests.post(root_url+'album', json=data, **requests_kwargs)
assert r.status_code in [200, 201]
album_to_id[album] = r.json()['id']
print(album, 'album added!')
cpt += 1
print(cpt, "albums created")
print(" 5. Adding assets to albums")
# Note: immich manage duplicates without problem,
# so we can each time ad all assets to same album, no photo will be duplicated
for album, assets in album_to_assets.items():
id = album_to_id[album]
data = {'ids':assets}
r = requests.put(root_url+f'album/{id}/assets', json=data, **requests_kwargs)
assert r.status_code in [200, 201]
response = r.json()
cpt = 0
for res in response:
if not res['success']:
if res['error'] != 'duplicate':
print("Warning, error in adding an asset to an album:", res['error'])
else:
cpt += 1
if cpt > 0:
print(f"{str(cpt).zfill(3)} new assets added to {album}")
print("Done!")
@ajnart
Copy link

ajnart commented Nov 28, 2023

Here is my proposed version

import requests
import os
from collections import defaultdict

# I have photos in subfolders like : 
# /mnt/media/Photos/2023-08 Holidays
# /mnt/media/Photos/2023-06 Birthday
# /mnt/media/Photos/2022-12 Christmas
# This script will create 3 albums
# 2023-08 Holidays, 2023-06 Birthday, 2022-12 Christmas
# And populate them with the photos inside
# The script can be run multiple times to update, new albums will be created,
# or new photos added in existing subfolder will be added to corresponding album 

# Personnalize here 
root_path = '/mnt/media/photos'
root_url = 'http://localhost:2283/api/'
api_key = ''
SPLIT_ALBUMS = True # if True, will create albums for each subfolder too
# /mnt/albums/2022/birthday
# /mnt/albums/2022/vacation    
# /mnt/albums/2023/birthday
# /mnt/albums/2023/vacation
# SPLIT_ALBUMS = True will create 6 total albums : 2022, 2022/birthday, 2022/vacation, 2023, 2023/birthday, 2023/vacation


requests_kwargs = {
    'headers' : {
        'x-api-key': api_key,
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    }
}
if root_path[-1] != '/':
    root_path = root_path + '/'
if root_url[-1] != '/':
    root_url = root_url + '/'


print("  1. Requesting all assets")
r = requests.get(root_url+'asset', **requests_kwargs)
assert r.status_code == 200
assets = r.json()
print(len(assets), "photos found")



print("  2. Sorting assets to corresponding albums using folder name")
album_to_assets = defaultdict(list)
for asset in assets:
    asset_path = asset['originalPath']
    if root_path not in asset_path:
        continue
    album_name = asset_path.replace(root_path, '').split('/')[0]
    album_to_assets[album_name].append(asset['id'])
    if (SPLIT_ALBUMS == True):
      album_name_split = '/'.join(asset_path.replace(root_path, '').split('/')[:2])
      album_to_assets[album_name_split].append(asset['id'])

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

print(len(album_to_assets), "albums identified")
print(list(album_to_assets.keys()))
print("Press Enter to continue, Ctrl+C to abort")
input()


album_to_id = {}

print("  3. Listing existing albums on immich")
r = requests.get(root_url+'album', **requests_kwargs)
assert r.status_code == 200
albums = r.json()
album_to_id = {album['albumName']:album['id'] for album in albums }
print(len(albums), "existing albums identified")


print("  4. Creating albums if needed")
cpt = 0
for album in album_to_assets:
    if album in album_to_id:
        continue
    data = {
        'albumName': album,
        'description': album
    }
    r = requests.post(root_url+'album', json=data, **requests_kwargs)
    assert r.status_code in [200, 201]
    album_to_id[album] = r.json()['id']
    print(album, 'album added!')
    cpt += 1
print(cpt, "albums created")


print("  5. Adding assets to albums")
# Note: immich manage duplicates without problem, 
# so we can each time ad all assets to same album, no photo will be duplicated 
for album, assets in album_to_assets.items():
    id = album_to_id[album]
    data = {'ids':assets}
    r = requests.put(root_url+f'album/{id}/assets', json=data, **requests_kwargs)
    assert r.status_code in [200, 201]
    response = r.json()

    cpt = 0
    for res in response:
        if not res['success']:
            if  res['error'] != 'duplicate':
                print("Warning, error in adding an asset to an album:", res['error'])
        else:
            cpt += 1
    if cpt > 0:
        print(f"{str(cpt).zfill(3)} new assets added to {album}")

print("Done!")

@REDVM
Copy link
Author

REDVM commented Nov 28, 2023

Nice 👍

@ajnart
Copy link

ajnart commented Nov 28, 2023

Nice 👍

I believe you can revision your Gist with my version if you'd like, like accepting a PR but on a gist 😆

@wiziwk
Copy link

wiziwk commented Dec 22, 2023

Hi, I am getting this error. Can you help look at this? Python v 2.7.13, running in Linux docker.

  File "immich_auto_album.py", line 93
    r = requests.put(root_url+f'album/{id}/assets', json=data, **requests_kwargs)

Edit: moved to python3 and issue solved.

@rysuq
Copy link

rysuq commented Dec 31, 2023

Great job!
If you are using docker you have to add docker path (based on your .env file):
root_path = '/usr/src/app/external'

@StarrrLiteNL
Copy link

If I run this script with SPLIT_ALBUMS set to true, it will identify many photo's as folder as well.
For some folders, it will create an (empty) album for every single file in that folder, causing the script to create 900+ albums where it should be about 50 or so.
It does not seem to do it for every folder though.

I am unsure why it does it, but my best guess is that not all my folders are nested to the same levels, some folders under my root will only have photo's, some have subfolders and others have both subfolders and photo's.

I am unfamiliar with python, so it's hard to say for me if this is indeed the cause of the issue or not.

If I set SPLIT_ALBUMS to false, it will only make albums for the top-level folders as expected, but I would love to have all folders (and just folders) as a seperate album.

@maivorbim
Copy link

maivorbim commented Feb 6, 2024

Hello, I am getting the following error:

  1. Adding assets to albums
    Traceback (most recent call last):
    File "/media/passport5TB/immich/ ./create_albums_from_folders_and_subfolders.py", line 102, in
    assert r.status_code in [200, 201]
    ^^^^^^^^^^^^^^^^^^^^
    AssertionError

Any ideas?

LE: this shows up in immich error log:

[Nest] 2971 - 02/06/2024, 4:16:02 PM ERROR [QueryFailedError: bind message has 45432 parameter formats but 0 parameters
at PostgresQueryRunner.query (/app/immich/server/node_modules/typeorm/driver/postgres/PostgresQueryRunner.js:219:19)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async InsertQueryBuilder.execute (/app/immich/server/node_modules/typeorm/query-builder/InsertQueryBuilder.js:106:33)
at async AlbumRepository.addAssets (/app/immich/server/dist/infra/repositories/album.repository.js:201:9)
at async AlbumService.addAssets (/app/immich/server/dist/domain/album/album.service.js:158:13)] Failed to add assets to album
[Nest] 2971 - 02/06/2024, 4:16:02 PM ERROR [QueryFailedError: bind message has 45432 parameter formats but 0 parameters
at PostgresQueryRunner.query (/app/immich/server/node_modules/typeorm/driver/postgres/PostgresQueryRunner.js:219:19)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async InsertQueryBuilder.execute (/app/immich/server/node_modules/typeorm/query-builder/InsertQueryBuilder.js:106:33)
at async AlbumRepository.addAssets (/app/immich/server/dist/infra/repositories/album.repository.js:201:9)
at async AlbumService.addAssets (/app/immich/server/dist/domain/album/album.service.js:158:13)] QueryFailedError: bind message has 45432 parameter formats but 0 parameters

@PubBow
Copy link

PubBow commented Feb 7, 2024

@ajnart Your script worked a treat however one issue is it doesn't process any further sub-directories. Example:

/mnt/albums/2023/vacations/1
/mnt/albums/2023/vacations/2
/mnt/albums/2023/vacations/3
/mnt/albums/2023/vacations/4
/mnt/albums/2023/vacations/5

It just creates /mnt/albums/2023/vacations/ and not the 5 directories under vacations (not that I'm lucky enough to have 5 vacations this is just an example) so there is just one big media list.

It's a shame that Immich doesn't have this as a standard feature for external libraries.

@Salvoxia
Copy link

Salvoxia commented Feb 9, 2024

@maivorbim

Hello, I am getting the following error:

5. Adding assets to albums
   Traceback (most recent call last):
   File "/media/passport5TB/immich/ ./create_albums_from_folders_and_subfolders.py", line 102, in 
   assert r.status_code in [200, 201]
   ^^^^^^^^^^^^^^^^^^^^
   AssertionError

Any ideas?

LE: this shows up in immich error log:

[Nest] 2971 - 02/06/2024, 4:16:02 PM ERROR [QueryFailedError: bind message has 45432 parameter formats but 0 parameters

Hi,

I was running into the same issue. That error happens if you have too many images in one folder. The script tries to add them to an album all in one go, which the API cannot cope with.

I worked around this by introducing a chunking mechanism that will keep adding smaller chunks of images to an album until its done. I'll post my script's version down below.
I also replaced the initial variables with command line arguments so it could be used with different API keys (for multiple users), and added arguments for running it without the check before creating albums so it can run as a cronjob.

Beware that my script is based on the original Gist, not the one that takes sub-folders into account (works better for my usecase).

import requests
import os
import argparse
from collections import defaultdict


# I have photos in subfolders like : 
# /mnt/media/Photos/2023-08 Holidays
# /mnt/media/Photos/2023-06 Birthday
# /mnt/media/Photos/2022-12 Christmas
# This script will create 3 albums
# 2023-08 Holidays, 2023-06 Birthday, 2022-12 Christmas
# And populate them with the photos inside
# The script can be run multiple times to update, new albums will be created,
# or new photos added in existing subfolder will be added to corresponding album 

# Personnalize here 
#root_path = '/my_external_lib'
#root_url = 'https://immich.mydomain.com/api/'
#api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

#number_of_images_per_request = 2000

parser = argparse.ArgumentParser(description="Create Immich Albums from an external library path based on the top level folders", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("root_path", help="The external libarary's root path in Immich")
parser.add_argument("api_url", help="The root API URL of immich")
parser.add_argument("api_key", help="The Immich API Key to use")
parser.add_argument("-u", "--unattended", action="store_true", help="The Immich API Key to use")
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")
args = vars(parser.parse_args())

root_path = args["root_path"]
root_url = args["api_url"]
api_key = args["api_key"]
number_of_images_per_request = args["chunk_size"]
unattended = args["unattended"]

# Yield successive n-sized 
# chunks from l. 
def divide_chunks(l, n): 
      
    # looping till length l 
    for i in range(0, len(l), n):  
        yield l[i:i + n] 
  

requests_kwargs = {
    'headers' : {
        'x-api-key': api_key,
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    }
}
if root_path[-1] != '/':
    root_path = root_path + '/'
if root_url[-1] != '/':
    root_url = root_url + '/'


print("  1. Requesting all assets")
r = requests.get(root_url+'asset', **requests_kwargs)
assert r.status_code == 200
assets = r.json()
print(len(assets), "photos found")



print("  2. Sorting assets to corresponding albums using folder name")
album_to_assets = defaultdict(list)
for asset in assets:
    asset_path = asset['originalPath']
    if root_path not in asset_path:
        continue
    album_name = asset_path.replace(root_path, '').split('/')[0]
    if "." not in album_name:
        album_to_assets[album_name].append(asset['id'])

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

print(len(album_to_assets), "albums identified")
print(list(album_to_assets.keys()))
if not unattended:
    print("Press Enter to continue, Ctrl+C to abort")
    input()


album_to_id = {}

print("  3. Listing existing albums on immich")
r = requests.get(root_url+'album', **requests_kwargs)
assert r.status_code == 200
albums = r.json()
album_to_id = {album['albumName']:album['id'] for album in albums }
print(len(albums), "existing albums identified")


print("  4. Creating albums if needed")
cpt = 0
for album in album_to_assets:
    if album in album_to_id:
        continue
    data = {
        'albumName': album,
        'description': album
    }
    r = requests.post(root_url+'album', json=data, **requests_kwargs)
    assert r.status_code in [200, 201]
    album_to_id[album] = r.json()['id']
    print(album, 'album added!')
    cpt += 1
print(cpt, "albums created")


print("  5. Adding assets to albums")
# Note: immich manage duplicates without problem, 
# so we can each time ad all assets to same album, no photo will be duplicated 
for album, assets in album_to_assets.items():
    id = album_to_id[album]
    
    assets_chunked = list(divide_chunks(assets, number_of_images_per_request))
    for assets_chunk in assets_chunked:
        data = {'ids':assets_chunk}
        r = requests.put(root_url+f'album/{id}/assets', json=data, **requests_kwargs)
        if r.status_code not in [200, 201]:
            print(album)
            print(r.json())
            print(data)
            continue
        assert r.status_code in [200, 201]
        response = r.json()

        cpt = 0
        for res in response:
            if not res['success']:
                if  res['error'] != 'duplicate':
                    print("Warning, error in adding an asset to an album:", res['error'])
            else:
                cpt += 1
        if cpt > 0:
            print(f"{str(cpt).zfill(3)} new assets added to {album}")

print("Done!")

@maivorbim
Copy link

Thanks a lot! I will try it and let you know if it fixed my problem.

@maivorbim
Copy link

Thanks! Worked great!

@Krovikan-Vamp
Copy link

Krovikan-Vamp commented Feb 13, 2024 via email

@Salvoxia
Copy link

@Krovikan-Vamp That happens when root_path in the script does is not found any asset's (= image's) path in the Immich container. Make sure to set root_path to the same value where the images for which albums should be created are available inside the container.
For example, I use the script for an external library that is mounted into the container as /external_libs/photos, so that's the value I use for root_path in the script.
Basically, it comes down to this piece in the script:

asset_path = asset['originalPath']
if root_path not in asset_path:
    continue

If root_path is not part of an images path, it gets discarded.

@haldi4803
Copy link

haldi4803 commented Feb 21, 2024

Uhm, why does it only get 1000 Pictures?
Is the API Limited to 1000 accesses? using v1.95.1 and docker. Running script on the host.

root@NAS:/volume3/SSD# python3 immichpy.py
  1. Requesting all assets
1000 photos found
  2. Sorting assets to corresponding albums using folder name
2 albums identified
['Schottland 2019.06', 'Sri Lanka 2019.08']
Press Enter to continue, Ctrl+C to abort

  3. Listing existing albums on immich
0 existing albums identified
  4. Creating albums if needed
Schottland 2019.06 album added!
Sri Lanka 2019.08 album added!
2 albums created
  5. Adding assets to albums
634 new assets added to Schottland 2019.06
366 new assets added to Sri Lanka 2019.08
Done!

@Salvoxia
Copy link

Salvoxia commented Feb 21, 2024

I haven't had a chance to update to v1.95.x yet, but I think it's very possible some kind of pagination mechanism was introduced to the assets API endpoint that is used to request all the image from Immich.
I'll take a closer look when I performed my upgrade and have had a closer look at what's going on.

@Salvoxia
Copy link

Salvoxia commented Feb 21, 2024

Alright, new script revision is in, compatible with the API changes introduced in v1.95.x, but also backwards compatible.
I forked this Gist for maintenance. My modified version is available here https://gist.github.com/Salvoxia/1a0074a7c7e8817e1e2ac5a4bf8af66c

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment