1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
|
from dataclasses import dataclass, asdict
from texture import ImageTextureImmichAsset
from manager import PixMan
@dataclass
class Album:
id: str
range_start: int
range_end: int
assets_list: list[str] = None
@property
def assets_count(self):
return end - start
@dataclass
class CallbackStateData:
asset_index: int
movement: int
assets: list[str]
current_asset: 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,
current_asset=assets[5],
)
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.max_cache_items = max_cache_items
self.radius_small = 10
self.cache_items_behind = 0
self.cache_items_ahead = 0
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
@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
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
return albums
def _get_album_assets(self):
assets = []
for album in self.albums:
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:
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.assets[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
#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(
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.last_movement = movement
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
# 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):
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")
return self._update_cache_get_item(index)
|