From e1a6fc09afc088dcb67263ed5923f5be41c32c31 Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Sun, 25 May 2025 21:38:37 -0500 Subject: use lazy caching texture list to limit number of images loaded at one time --- lazycachelist.py | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 lazycachelist.py (limited to 'lazycachelist.py') diff --git a/lazycachelist.py b/lazycachelist.py new file mode 100644 index 0000000..0c37ba3 --- /dev/null +++ b/lazycachelist.py @@ -0,0 +1,126 @@ +from dataclasses import dataclass + +from texture import ImageTextureImmichAsset + +@dataclass +class Album: + id: str + range_start: int + range_end: int + assets_list: list[str] = None + + @property + def assets_count(self): + return end - start + + +class LazyCachingTextureList(): + def __init__(self, immich_connector, album_ids, max_cache_items=100): + self.immich_connector = immich_connector + assert max_cache_items >= 20, "Minimum cache items is 20" # Double small radius + + # Ring buffer + self.cache_length = max_cache_items + self.cache = [None] * max_cache_items + self.radius_small = 10 + self.radius_large = int(max_cache_items / 2) + self.cache_items_behind = 0 + self.cache_items_ahead = 0 + self.cache_index = 0 + self.asset_index = 0 + self.asset_count = 0 + + self.cached_items = 0 + + self.album_keys = album_ids + self.albums = self._get_albums() + + def index_in_cache_range(self, index): + index_range_low = (self.asset_index - self.cache_items_behind) % self.asset_count + index_range_high = (self.asset_index + self.cache_items_ahead ) % self.asset_count + if index_range_low > index_range_high: + return index_range_low <= index or index <= index_range_high + return index_range_low <= index <= index_range_high + + def _get_albums(self): + albums = [] + self.asset_count = i = 0 + albums_info = self.immich_connector.load_all_albums() + for album_info in albums_info: + id = album_info["id"] + if id not in self.album_keys: + continue + asset_count = album_info["assetCount"] + albums.append(Album(id, i, i + asset_count)) + i += asset_count + 1 + self.asset_count += asset_count + #self.asset_count = sum(( album.assets_count for album in self.albums )) + return albums + + def _get_album_asset_by_index(self, asset_index): + if asset_index < 0: + asset_index = asset_count - asset_index + for album in self.albums: + if album.range_start <= asset_index <= album.range_end: + if album.assets_list is None: + album.assets_list = self.immich_connector.load_album_assets(album.id) + return album.assets_list[asset_index - album.range_start] + raise IndexError("Index out of bounds") + + def _fill_cache(self, current_radius, final_radius, step): + if current_radius >= final_radius: + return current_radius + + for i in range(current_radius * step, final_radius * step, step): + cache_index = (self.cache_index + i) % self.cache_length + asset_index = (self.asset_index + i) % self.asset_count + if self.cache[cache_index]: + self.cache[cache_index].free() # Since this is a ring buffer, textures outside of range can just get free'd here + asset = self._get_album_asset_by_index(asset_index) + tex = ImageTextureImmichAsset(asset, asset_index) + self.immich_connector.load_texture_async(self, tex) + self.cache[cache_index] = tex + self.cached_items += 1 + + return max(current_radius, final_radius) + + def _update_cache_get_item(self, asset_index): + prev_asset_index = self.asset_index + self.asset_index = asset_index % self.asset_count + # Movement is the distance between the previous and current index in the assets list + # Since the list wraps around, fastest method is just to get the abs min of the 3 cases + movement = min( + asset_index - prev_asset_index, # No list wrap + asset_index - self.asset_count, # Wrap backwards (0 -> -1) + self.asset_count - prev_asset_index, # Wrap forwards (-1 -> 0) + key=abs) + self.cache_index = (self.cache_index + movement) % self.cache_length + + ahead = max(0, self.cache_items_ahead - movement) + behind = max(0, self.cache_items_behind + movement) + + if ahead + behind > self.cache_length: + if movement < 0: + ahead += movement + else: + behind -= movement + + #print("AHEAD/BEHIND/CACHE_I/ASSET_I/MOVEMENT:", ahead, behind, self.cache_index, self.asset_index, movement) + # TODO if ahead is 0 then clear queue + + ahead = self._fill_cache(ahead, self.radius_small, +1) # Fill small radius ahead of cache_index + behind = self._fill_cache(behind, self.radius_small, -1) # Fill small radius behind cache_index + ahead = self._fill_cache(ahead, self.radius_large, +1) # Fill large radius ahead of cache_index + + self.cache_items_ahead = ahead + self.cache_items_behind = behind + return self.cache[self.cache_index] + + def __len__(self): + return self.asset_count + + def __getitem__(self, index): + i = index % self.asset_count + if abs(index) > i: + raise IndexError("Index out of bounds") + return self._update_cache_get_item(index) -- cgit v1.2.3 From cd1657ece1fa199964abd6544b81b394ab9369aa Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Sun, 15 Jun 2025 15:06:34 -0500 Subject: callbacks on lctl, websocket controls --- lazycachelist.py | 51 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 12 deletions(-) (limited to 'lazycachelist.py') diff --git a/lazycachelist.py b/lazycachelist.py index 0c37ba3..b08b85d 100644 --- a/lazycachelist.py +++ b/lazycachelist.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, asdict from texture import ImageTextureImmichAsset @@ -14,8 +14,27 @@ class Album: return end - start +@dataclass +class CallbackStateData: + asset_index: int + movement: int + assets: list[str] + + @classmethod + def from_lctl(cls, l): + si = (l.asset_index - 5) % l.asset_count + ei = (l.asset_index + 6) % l.asset_count + sa = l.assets[si:ei] if si < ei else l.assets[si:] + l.assets[:ei] + assets = [ a["id"] for a in sa ] + return cls( + asset_index=l.asset_index, + movement=l.last_movement, + assets=assets, + ) + + class LazyCachingTextureList(): - def __init__(self, immich_connector, album_ids, max_cache_items=100): + def __init__(self, immich_connector, album_ids, max_cache_items=100, change_callback=None): self.immich_connector = immich_connector assert max_cache_items >= 20, "Minimum cache items is 20" # Double small radius @@ -29,11 +48,16 @@ class LazyCachingTextureList(): self.cache_index = 0 self.asset_index = 0 self.asset_count = 0 + self.last_movement = 0 self.cached_items = 0 self.album_keys = album_ids + # TODO simplify album handling, dont need classes, etc. self.albums = self._get_albums() + self.assets = self._get_album_assets() + + self.change_callback = change_callback def index_in_cache_range(self, index): index_range_low = (self.asset_index - self.cache_items_behind) % self.asset_count @@ -54,18 +78,13 @@ class LazyCachingTextureList(): albums.append(Album(id, i, i + asset_count)) i += asset_count + 1 self.asset_count += asset_count - #self.asset_count = sum(( album.assets_count for album in self.albums )) return albums - def _get_album_asset_by_index(self, asset_index): - if asset_index < 0: - asset_index = asset_count - asset_index + def _get_album_assets(self): + assets = [] for album in self.albums: - if album.range_start <= asset_index <= album.range_end: - if album.assets_list is None: - album.assets_list = self.immich_connector.load_album_assets(album.id) - return album.assets_list[asset_index - album.range_start] - raise IndexError("Index out of bounds") + assets += self.immich_connector.load_album_assets(album.id) + return assets def _fill_cache(self, current_radius, final_radius, step): if current_radius >= final_radius: @@ -76,7 +95,7 @@ class LazyCachingTextureList(): asset_index = (self.asset_index + i) % self.asset_count if self.cache[cache_index]: self.cache[cache_index].free() # Since this is a ring buffer, textures outside of range can just get free'd here - asset = self._get_album_asset_by_index(asset_index) + asset = self.assets[asset_index] tex = ImageTextureImmichAsset(asset, asset_index) self.immich_connector.load_texture_async(self, tex) self.cache[cache_index] = tex @@ -87,6 +106,8 @@ class LazyCachingTextureList(): def _update_cache_get_item(self, asset_index): prev_asset_index = self.asset_index self.asset_index = asset_index % self.asset_count + #if prev_asset_index == asset_index: + # return self.cache[self.cache_index] # Movement is the distance between the previous and current index in the assets list # Since the list wraps around, fastest method is just to get the abs min of the 3 cases movement = min( @@ -94,6 +115,7 @@ class LazyCachingTextureList(): asset_index - self.asset_count, # Wrap backwards (0 -> -1) self.asset_count - prev_asset_index, # Wrap forwards (-1 -> 0) key=abs) + self.last_movement = movement self.cache_index = (self.cache_index + movement) % self.cache_length ahead = max(0, self.cache_items_ahead - movement) @@ -114,6 +136,11 @@ class LazyCachingTextureList(): self.cache_items_ahead = ahead self.cache_items_behind = behind + + # Perform callback + if prev_asset_index != asset_index and self.change_callback: + self.change_callback(asdict(CallbackStateData.from_lctl(self))) + return self.cache[self.cache_index] def __len__(self): -- cgit v1.2.3 From b8df4605b42d9a61bb4ae4731efabbdc38166063 Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Mon, 16 Jun 2025 21:50:38 -0500 Subject: add config and add application thread manager --- lazycachelist.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'lazycachelist.py') diff --git a/lazycachelist.py b/lazycachelist.py index b08b85d..3b2744d 100644 --- a/lazycachelist.py +++ b/lazycachelist.py @@ -1,6 +1,7 @@ from dataclasses import dataclass, asdict from texture import ImageTextureImmichAsset +from manager import PixMan @dataclass class Album: @@ -34,8 +35,8 @@ class CallbackStateData: class LazyCachingTextureList(): - def __init__(self, immich_connector, album_ids, max_cache_items=100, change_callback=None): - self.immich_connector = immich_connector + def __init__(self, album_ids, max_cache_items=100, change_callback=None): + self.immich_connector = PixMan().immich_connector assert max_cache_items >= 20, "Minimum cache items is 20" # Double small radius # Ring buffer -- cgit v1.2.3 From ce64f1a42c9570efa75cc2f568e59d683f499bdd Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Fri, 20 Jun 2025 22:32:28 -0500 Subject: config update and more endpoints for api fromtend --- lazycachelist.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lazycachelist.py') diff --git a/lazycachelist.py b/lazycachelist.py index 3b2744d..5243def 100644 --- a/lazycachelist.py +++ b/lazycachelist.py @@ -20,6 +20,7 @@ class CallbackStateData: asset_index: int movement: int assets: list[str] + current_asset: str @classmethod def from_lctl(cls, l): @@ -31,6 +32,7 @@ class CallbackStateData: asset_index=l.asset_index, movement=l.last_movement, assets=assets, + current_asset=assets[5], ) -- cgit v1.2.3 From 2f03f39e24053377dce108e45fde13ccd1e0ae22 Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Tue, 24 Jun 2025 18:56:53 -0500 Subject: window can now handle no selected albums --- lazycachelist.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lazycachelist.py') diff --git a/lazycachelist.py b/lazycachelist.py index 5243def..99212ab 100644 --- a/lazycachelist.py +++ b/lazycachelist.py @@ -150,6 +150,8 @@ class LazyCachingTextureList(): return self.asset_count def __getitem__(self, index): + if not self.asset_count: + raise IndexError("Index out of bounds") i = index % self.asset_count if abs(index) > i: raise IndexError("Index out of bounds") -- cgit v1.2.3 From 4c3d572eb850c32a45ec9cbaf82688d45c1eebf4 Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Tue, 24 Jun 2025 19:16:36 -0500 Subject: add ability to change out albums list during runtime --- lazycachelist.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) (limited to 'lazycachelist.py') diff --git a/lazycachelist.py b/lazycachelist.py index 99212ab..b4d003a 100644 --- a/lazycachelist.py +++ b/lazycachelist.py @@ -39,13 +39,12 @@ class CallbackStateData: class LazyCachingTextureList(): def __init__(self, album_ids, max_cache_items=100, change_callback=None): self.immich_connector = PixMan().immich_connector + self.void = False assert max_cache_items >= 20, "Minimum cache items is 20" # Double small radius # Ring buffer - self.cache_length = max_cache_items - self.cache = [None] * max_cache_items + self.max_cache_items = max_cache_items self.radius_small = 10 - self.radius_large = int(max_cache_items / 2) self.cache_items_behind = 0 self.cache_items_ahead = 0 self.cache_index = 0 @@ -62,6 +61,19 @@ class LazyCachingTextureList(): self.change_callback = change_callback + @property + def max_cache_items(self): + return self.cache_length + + @max_cache_items.setter + def max_cache_items(self, max_cache_items): + self.cache_length = max_cache_items + self.cache = [None] * max_cache_items + self.radius_large = int(max_cache_items / 2) + + def free(self): + self.void = True + def index_in_cache_range(self, index): index_range_low = (self.asset_index - self.cache_items_behind) % self.asset_count index_range_high = (self.asset_index + self.cache_items_ahead ) % self.asset_count -- cgit v1.2.3