Skip to content

refactor: integrate immichpy client#259

Merged
Salvoxia merged 34 commits into
Salvoxia:mainfrom
timonrieger:feat/immich-sdk
Feb 18, 2026
Merged

refactor: integrate immichpy client#259
Salvoxia merged 34 commits into
Salvoxia:mainfrom
timonrieger:feat/immich-sdk

Conversation

@timonrieger

@timonrieger timonrieger commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

This change removes legacy API compatibility and standardizes all API interaction through the immichpy client.

What changed

  • All direct/legacy API handling has been removed
  • The project now relies exclusively on modern Immich API paths
  • All requests are routed through immichpy

User impact

  • No functional or UX changes expected
  • This is an internal refactor affecting only API integration

Compatibility

Because backward compatibility has been removed, this should ship as a major version bump.

Manual testing completed; results attached.

Test Details
In [1]: from immich_auto_album import ApiClient, AlbumModel
   ...: from immich.client.generated import (
   ...:     AddUsersDto,
   ...:     AlbumResponseDto,
   ...:     AlbumUserAddDto,
   ...:     AlbumUserRole,
   ...:     AssetBulkDeleteDto,
   ...:     AssetBulkUpdateDto,
   ...:     AssetResponseDto,
   ...:     AssetVisibility,
   ...:     BulkIdsDto,
   ...:     CreateAlbumDto,
   ...:     LibraryResponseDto,
   ...:     MetadataSearchDto,
   ...:     SearchResponseDto,
   ...:     ServerVersionResponseDto,
   ...:     UpdateAlbumDto,
   ...:     UpdateAlbumUserDto,
   ...:     UserResponseDto,
   ...: )

In [2]: c = ApiClient(api_url="https://demo.immich.app/api" , api_key="j0cjmKL7WJeGPPjEMp1Qy9ApOEdwEqCKRRo4OKsp0")

In [3]: c.fetch_server_version()
Out[3]: ServerVersionResponseDto(major=2, minor=5, patch=2)

In [4]: albums = c.fetch_albums()
   ...: albums[0].id   # example album id
Out[4]: 'afc95f17-f9cb-4359-8b87-9bfc3481cd41'

In [5]: c.fetch_album_info(albums[0].id)
Out[5]: AlbumResponseDto(album_name='TestPB', album_thumbnail_asset_id='c158abf9-690f-4cf2-8537-a344c041b8db', album_users=[], asset_count=5, assets=[AssetResponseDto(checksum='wqxdz3cL9WrINUwH4POekeOA6Kg=', created_at=datetime.datetime(2026, 1, 20, 15, 20, 15, 281789, tzinfo=TzInfo(0)), device_asset_id='2a4f9698-5f2a-4432-8489-0530688026d7.jpg-5356409', device_id='CLI', duplicate_id=None, duration='0:00:00.00000', exif_info=ExifResponseDto(city='Rakitna', country='Slovenia', date_time_original=datetime.datetime(2025, 4, 5, 15, 40, 3, tzinfo=TzInfo(0)), description='Path trough the forest', exif_image_height=5470, exif_image_width=3646, exposure_time='1/500', f_number=10, file_size_in_byte=5356409, focal_length=55, iso=3200, latitude=45.841414, lens_model=None, longitude=14.493183, make='SONY', model='ILCE-6400', modify_date=datetime.datetime(2025, 5, 1, 14, 27, 20, 294000, tzinfo=TzInfo(0)), orientation='1', projection_type=None, rating=None, state='Brezovica', time_zone='Europe/Ljubljana'), file_created_at=datetime.datetime(2025, 4, 5, 15, 40, 3, tzinfo=TzInfo(0)), file_modified_at=datetime.datetime(2025, 5, 1, 14, 27, 20, 294000, tzinfo=TzInfo(0)), has_metadata=True, height=5470, id='2c5bb067-8541-407d-8d31-2f9ce6f30e2c', is_archived=False, is_edited=False, is_favorite=True, is_offline=False, is_trashed=False, library_id=None, live_photo_video_id=None, local_date_time=datetime.datetime(2025, 4, 5, 17, 40, 3, tzinfo=TzInfo(0)), original_file_name='2a4f9698-5f2a-4432-8489-0530688026d7.jpg', original_mime_type='image/jpeg', original_path='/data/upload/6bbe2767-7851-461a-aa2d-afbd3460aa85/d8/a7/d8a7b22d-72c7-43dd-a41a-13cd57b61639.jpg', owner=None, owner_id='6bbe2767-7851-461a-aa2d-afbd3460aa85', people=[], resized=True, stack=None, tags=None, thumbhash='SOgFDQAKyIV4aJh3iHiIiBiAew9V', type=<AssetTypeEnum.IMAGE: 'IMAGE'>, unassigned_faces=None, updated_at=datetime.datetime(2026, 1, 29, 21, 14, 25, 699728, tzinfo=TzInfo(0)), visibility=<AssetVisibility.TIMELINE: 'timeline'>, width=3646)], contributor_counts=[ContributorCountResponseDto(asset_count=5, user_id='6bbe2767-7851-461a-aa2d-afbd3460aa85')], created_at=datetime.datetime(2026, 1, 29, 20, 34, 21, 60000, tzinfo=TzInfo(0)), description='', end_date=datetime.datetime(2025, 4, 5, 0, 0, tzinfo=TzInfo(0)), has_shared_link=True, id='afc95f17-f9cb-4359-8b87-9bfc3481cd41', is_activity_enabled=True, last_modified_asset_timestamp=datetime.datetime(2026, 1, 29, 21, 14, 25, 699000, tzinfo=TzInfo(0)), order=<AssetOrder.DESC: 'desc'>, owner=UserResponseDto(avatar_color=<UserAvatarColor.YELLOW: 'yellow'>, email='demo@immich.app', id='6bbe2767-7851-461a-aa2d-afbd3460aa85', name='Jane Doe', profile_changed_at=datetime.datetime(2026, 1, 20, 15, 13, 30, 207092, tzinfo=TzInfo(0)), profile_image_path=''), owner_id='6bbe2767-7851-461a-aa2d-afbd3460aa85', shared=True, start_date=datetime.datetime(2025, 3, 10, 0, 0, tzinfo=TzInfo(0)), updated_at=datetime.datetime(2026, 1, 29, 20, 34, 32, 598000, tzinfo=TzInfo(0)))


In [4]: c = ApiClient(api_url="https://immich.fam.timonrieger.de/api" , api_key="2gywRgJo2UXEcRGT1B0QeKjhhA6PIByv4xKQtufMo4E")

In [5]: c.fetch_assets(True, [])
Out[5]: 
[AssetResponseDto(checksum='rOW5+hqbLpQ578OnBog2TfTQQYc=', created_at=datetime.datetime(2026, 1, 18, 20, 37, 51, 950000, tzinfo=TzInfo(0)), device_asset_id='BwOhb-GIT8w_processed.jpg-2379968', device_id='immichpy', duplicate_id=None, duration='0:00:00.00000', exif_info=None, file_created_at=datetime.datetime(2026, 1, 19, 1, 44, 2, tzinfo=TzInfo(0)), file_modified_at=datetime.datetime(2025, 5, 1, 12, 27, 18, tzinfo=TzInfo(0)), has_metadata=True, id='64f41ad7-e83e-4e65-a8b8-49b13939d0f3', is_archived=False, is_favorite=False, is_offline=False, is_trashed=False, library_id=None, live_photo_video_id=None, local_date_time=datetime.datetime(2026, 1, 19, 1, 44, 2, tzinfo=TzInfo(0)), original_file_name='BwOhb-GIT8w_processed.jpg', original_mime_type='image/jpeg', original_path='/data/library/51fc3e00-57fc-4f0a-afc8-e1c085f21089/2026/01/BwOhb-GIT8w_processed.jpg', owner=None, owner_id='51fc3e00-57fc-4f0a-afc8-e1c085f21089', people=[], resized=True, stack=None, tags=None, thumbhash='B8gFBYJleHd/d4h3eXh3iICHlwqH', type=<AssetTypeEnum.IMAGE: 'IMAGE'>, unassigned_faces=None, updated_at=datetime.datetime(2026, 1, 26, 14, 9, 29, 976000, tzinfo=TzInfo(0)), visibility=<AssetVisibility.TIMELINE: 'timeline'>)
 AssetResponseDto(checksum='NuR+wo+MoK1JV1FV7QK+IuxdTVQ=', created_at=datetime.datetime(2026, 1, 9, 18, 51, 58, 536000, tzinfo=TzInfo(0)), device_asset_id='web-PIC_20230610_122423_20230610162617.jpg-1767984506791', device_id='WEB', duplicate_id=None, duration='0:00:00.00000', exif_info=None, file_created_at=datetime.datetime(2026, 1, 9, 18, 48, 26, 790000, tzinfo=TzInfo(0)), file_modified_at=datetime.datetime(2026, 1, 9, 18, 48, 26, 791000, tzinfo=TzInfo(0)), has_metadata=True, id='9e637f8c-4191-45e6-ac9e-dbf51fa7d149', is_archived=True, is_favorite=False, is_offline=False, is_trashed=False, library_id=None, live_photo_video_id=None, local_date_time=datetime.datetime(2026, 1, 9, 18, 48, 26, 790000, tzinfo=TzInfo(0)), original_file_name='PIC_20230610_122423_20230610162617.jpg', original_mime_type='image/jpeg', original_path='/data/library/51fc3e00-57fc-4f0a-afc8-e1c085f21089/2026/01/PIC_20230610_122423_20230610162617.jpg', owner=None, owner_id='51fc3e00-57fc-4f0a-afc8-e1c085f21089', people=[], resized=True, stack=None, tags=None, thumbhash='IugJFwSqVYiIeIeIeIiZ9ohoiAN4NnAH', type=<AssetTypeEnum.IMAGE: 'IMAGE'>, unassigned_faces=None, updated_at=datetime.datetime(2026, 1, 25, 21, 22, 45, 693000, tzinfo=TzInfo(0)), visibility=<AssetVisibility.ARCHIVE: 'archive'>)]

In [7]: album_id = c.create_album("Test Album From REPL")

In [8]: album_id
Out[8]: '39b9797d-e8a4-477e-a8d0-c77ce1728c63'

In [9]: assets = c.fetch_assets_with_options(MetadataSearchDto(isNotInAlbum=False))

In [10]: len(assets)
Out[10]: 21

In [11]: c.set_album_thumb(album_id, 'c3d55e6f-d02d-47cb-b6ab-0058da99f596')

In [12]: c.delete_album(album_id)
Out[12]: True

In [13]: album_id = c.create_album("Another Test")

In [14]: asset_ids = [a.id for a in c.fetch_assets(False, [])[:5]]

In [15]: c.add_assets_to_album(album_id, asset_ids)
Out[15]: 
['64f41ad7-e83e-4e65-a8b8-49b13939d0f3',
 'ad89a026-f148-49a8-b529-3f33dbb3f292',
 'e45454c6-4153-4f27-93e4-c8e780e36caa',
 'c2db2985-690e-4674-87d1-a803c5925954',
 '773b5aee-76b9-4dfa-87fa-4225009f4e3e']
 
 In [16]: c.fetch_users()
Out[16]: 
[UserResponseDto(avatar_color=<UserAvatarColor.GREEN: 'green'>, email='s@s.s', id='51fc3e00-57fc-4f0a-afc8-e1c085f21089', name='s', profile_changed_at=datetime.datetime(2026, 1, 25, 23, 29, 52, 171000, tzinfo=TzInfo(0)), profile_image_path='/data/profile/51fc3e00-57fc-4f0a-afc8-e1c085f21089/07c3faa7-4c4e-41de-8abe-ce170a24f7d1.png')]
# redacted more users

In [17]: c.fetch_libraries()
Out[17]: [LibraryResponseDto(asset_count=0, created_at=datetime.datetime(2025, 11, 10, 17, 12, 3, 432000, tzinfo=TzInfo(0)), exclusion_patterns=['**/@eaDir/**', '**/._*', '**/#recycle/**', '**/#snapshot/**'], id='098adb6d-0a9b-494a-bd66-2ae6c0bf28ff', import_paths=['/srv/media/'], name='My Library', owner_id='627c136b-3bdd-4b71-aa9f-e7e57ff728c1', refreshed_at=datetime.datetime(2026, 1, 29, 23, 0, 1, 148000, tzinfo=TzInfo(0)), updated_at=datetime.datetime(2026, 1, 29, 23, 0, 1, 155000, tzinfo=TzInfo(0)))]

In [18]: users = c.fetch_users()

In [19]: album_id = c.fetch_albums()[0].id

In [20]: user_id = users[0].id

In [21]: c.share_album_with_user_and_role(album_id, [user_id], AlbumUserRole.VIEWER)

In [22]: c.update_album_share_user_role(album_id, user_id, AlbumUserRole.EDITOR)

In [23]: c.unshare_album_with_user(album_id, user_id)

In [24]: asset_ids = [c.fetch_assets(False, [])[0].id]

In [25]: asset_ids
Out[25]: ['64f41ad7-e83e-4e65-a8b8-49b13939d0f3']

In [26]: c.set_assets_visibility(asset_ids, AssetVisibility.ARCHIVE)

In [27]: c.fetch_albums()
Out[27]: [AlbumResponseDto(album_name='Another Test', album_thumbnail_asset_id=None, album_users=[], asset_count=0, assets=[], contributor_counts=None, created_at=datetime.datetime(2026, 1, 30, 0, 1, 48, 561000, tzinfo=TzInfo(0)), description='', end_date=None, has_shared_link=False, id='606bf715-2795-40da-acdb-d193fed004f1', is_activity_enabled=True, last_modified_asset_timestamp=None, order=<AssetOrder.DESC: 'desc'>, owner=UserResponseDto(avatar_color=<UserAvatarColor.GREEN: 'green'>, email='s@s.s', id='51fc3e00-57fc-4f0a-afc8-e1c085f21089', name='s', profile_changed_at=datetime.datetime(2026, 1, 25, 23, 29, 52, 171000, tzinfo=TzInfo(0)), profile_image_path='/data/profile/51fc3e00-57fc-4f0a-afc8-e1c085f21089/07c3faa7-4c4e-41de-8abe-ce170a24f7d1.png'), owner_id='51fc3e00-57fc-4f0a-afc8-e1c085f21089', shared=False, start_date=None, updated_at=datetime.datetime(2026, 1, 30, 0, 1, 48, 561000, tzinfo=TzInfo(0)))]

In [28]: c.delete_all_albums("timeline", True)

In [29]: c.fetch_albums()
Out[29]: []

after step 7
image

after 11
image

after 21
image

after 22
image

after 23:
image

after 26
image

Comment thread immich_auto_album.py
@Salvoxia

Copy link
Copy Markdown
Owner

Hi,

thank you for work!
If I understand correctly the immich-py client is your personal project and you are the sole maintainer? I hope you understand I'm a bit hesitant to introduce such a dependency. What's going to happen if you lose interest in the future and the library won't get any more updates?

By consciously not relying on any API libraries I tried to minimize dependencies and maintenance effort, while hoping that using only a few API calls with specific parameters would make the API implementation more resilient against changes, keeping compatibility over a bigger range of Immich Server versions.

Don't get me wrong, your project looks great, it's got proper documentation and tests! But it's very young, you seem to be the only contributor and I don't know how long it will exist. So can you convince me that the Folder Album Creator can rely on immich-py as long as there's still a use case for it? Maybe with the introduction of Immich workflows Immich will be able to do natively what this script does, who knows?

Kind Regards
Salvoxia

@timonrieger

timonrieger commented Jan 30, 2026

Copy link
Copy Markdown
Contributor Author

Hi,

Thanks for the thoughtful reply – your hesitation makes complete sense, and honestly I'd have the same concern introducing a single-maintainer dependency into a small, focused project like this.

A few points that might help clarify how I see immichpy in this context:

  1. It's intentionally thin and schema-driven
    The client isn't a hand-written abstraction layer – it's autogenerated directly from the Immich OpenAPI schema using the well-know and stable openapi-generator tool. That means its core is mostly models and endpoint mappings, not complex custom logic. If I ever stopped maintaining it, the project would still be relatively straightforward to fork or regenerate from the schema, since it doesn't hide the API behind heavy abstractions.

  2. The goal is API stability, not features
    My focus with immichpy is to track Immich API changes quickly and keep version compatibility aligned with server releases. The idea is less "new capabilities" and more acting as a buffer so downstream tools don't need to react to every API adjustment themselves.

  3. Low lock-in by design
    Using the client doesn't fundamentally change how your script works – it just replaces manual request/response handling. If at some point you decided to drop the dependency, the calls map quite directly back to raw API requests, so it's not an architectural dead end.

  4. Longevity
    Totally fair concern. My goal with immichpy isn't just to support a single script, but to grow it into a reliable, shared API layer for Python projects in the Immich ecosystem (automation tools, integrations, HA, etc.). That broader scope is a big part of what drives me to keep it closely aligned with Immich releases. While the library itself is still young, the foundations are quite stable: the code generation approach has been proven for a long time, and the client structure follows patterns I'm already using in other API clients that run in production systems. So the main moving part is tracking API changes, not evolving internal architecture.

  5. Automation & maintenance model
    A lot of the risk of a client going stale is mitigated by automation. The project has a full test suite, automated documentation generation, and release sync automation. On each Immich release, a PR is generated automatically to sync the client with the updated API schema. Patch releases are typically a one-click merge, and minor releases usually only need very small adjustments. This setup is specifically there to keep the client in lockstep with the API without relying on large manual effort. Check the docs, the last minor release PR and this patch release PR.

  6. Involvement in the ecosystem
    I'm also part of the wider Immich Team, involved in the ecosystem and contribute time and effort around the core project. My incentives are therefore closely aligned with where the API and platform are heading. This client grew out of that involvement rather than being an isolated side project, and the intention is for it to be a broadly usable API layer rather than something tied to one tool.

Regarding workflows: I've asked the team for more insight into what that feature is expected to cover, so it'll be interesting to see how that evolves. If Immich eventually handles this kind of use case natively, that's honestly a good outcome – in that case the script (and the importance of this dependency decision) likely becomes smaller anyway.

If you'd prefer to keep the current approach for now, I completely understand. I'm happy to revisit later or help in other ways – my main aim is just to reduce duplicated API maintenance effort where it makes sense, not to force a dependency.

Best,
Timon

@timonrieger

timonrieger commented Jan 30, 2026

Copy link
Copy Markdown
Contributor Author

our dialog motivated me to write the rationale for immichpy

@timonrieger timonrieger changed the title refactor: integrate immich-py client refactor: integrate immichpy client Jan 31, 2026
@Salvoxia

Copy link
Copy Markdown
Owner

Alright, you convinced me. Points 2 and 3 are essentially what I'm after as well, and points 5 and 6 do help :)
Let me play around with it and do a review.

Thanks again for the work you put into this!

Comment thread immich_auto_album.py
Comment thread immich_auto_album.py
Comment thread immich_auto_album.py
Comment thread README.md

@Salvoxia Salvoxia left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry it took me a while. 90% if your changes are pretty straight forward (if one is familiar with the DTOs).

@Salvoxia Salvoxia left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My default test runs worked as expected, except for album sharing. Could you please take a look at that?

Comment thread immich_auto_album.py Outdated
Comment thread immich_auto_album.py
Comment thread immich_auto_album.py
@Salvoxia

Copy link
Copy Markdown
Owner

Quick update: Please bear with me, I hope to find more time to complete this towards or at the next weekend!

@Salvoxia Salvoxia left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your patience! I've managed to put in another round of testing, hit another snag with album sharing when using album property inheritance

Comment thread immich_auto_album.py
Comment thread immich_auto_album.py
Comment thread immich_auto_album.py Outdated

@Salvoxia Salvoxia left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know what? I can't find anything else. If you can fix the linting errors we're good to go!

@timonrieger

Copy link
Copy Markdown
Contributor Author

okay should be good

@Salvoxia Salvoxia merged commit 91ace3c into Salvoxia:main Feb 18, 2026
6 checks passed
@timonrieger timonrieger deleted the feat/immich-sdk branch February 18, 2026 21:24
@timonrieger

Copy link
Copy Markdown
Contributor Author

Thanks for the great iterative work on this from both sides!

@Salvoxia

Copy link
Copy Markdown
Owner

Right back at you, thanks for the great collaboration. Here's to immichpy gaining traction and become an established (or THE) Python library for working with the Immich API!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants