spotifywebapiPython 1.0.243__py3-none-any.whl → 1.0.251__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. spotifywebapipython/const.py +1 -1
  2. spotifywebapipython/docs/__init__.html +56 -2
  3. spotifywebapipython/docs/index.html +5 -2
  4. spotifywebapipython/docs/search.js +1 -1
  5. spotifywebapipython/docs/spotifywebapipython/const.html +3 -3
  6. spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
  7. spotifywebapipython/docs/spotifywebapipython/sautils.html +2 -2
  8. spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +2 -2
  9. spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +2 -2
  10. spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +2 -2
  11. spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +235 -4
  12. spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +2 -2
  13. spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
  14. spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
  15. spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +2 -2
  16. spotifywebapipython/models/__init__.py +2 -0
  17. spotifywebapipython/models/episodesimplified.py +41 -0
  18. spotifywebapipython/models/imagepalettecolors.py +111 -0
  19. spotifywebapipython/models/imagevibrantcolors.py +2 -0
  20. spotifywebapipython/models/pageobject.py +1 -0
  21. spotifywebapipython/spotifyclient.py +219 -6
  22. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +227 -76
  23. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +2 -2
  24. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonecontrollerlistener.py +247 -0
  25. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonemanagerlistener.py +384 -0
  26. spotifywebapipython/vibrant/__init__.py +2 -0
  27. spotifywebapipython/vibrant/colorthieffast.py +626 -0
  28. {spotifywebapipython-1.0.243.dist-info → spotifywebapipython-1.0.251.dist-info}/METADATA +3 -2
  29. {spotifywebapipython-1.0.243.dist-info → spotifywebapipython-1.0.251.dist-info}/RECORD +32 -29
  30. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonelistener.py +0 -212
  31. {spotifywebapipython-1.0.243.dist-info → spotifywebapipython-1.0.251.dist-info}/LICENSE +0 -0
  32. {spotifywebapipython-1.0.243.dist-info → spotifywebapipython-1.0.251.dist-info}/WHEEL +0 -0
  33. {spotifywebapipython-1.0.243.dist-info → spotifywebapipython-1.0.251.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,626 @@
1
+ # Copyright 2025-2025 JiangLong Jia
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+ __version__ = '1.1.0'
20
+
21
+ import functools
22
+ from typing import Dict, Callable, List, Tuple, Optional, Union
23
+
24
+ import colorsys
25
+ import io
26
+ import numpy as np
27
+ from PIL import Image
28
+ import requests
29
+
30
+
31
+ class ColorThiefFast:
32
+ """
33
+ Color thief main class.
34
+ """
35
+ RGB = Tuple[int, int, int]
36
+
37
+ def __init__(self, image: Union[str, Image.Image, np.ndarray], return_percent: bool = False):
38
+ self.image: np.ndarray = self.process_image(image)
39
+ self.return_percent = return_percent
40
+
41
+
42
+ def process_image(
43
+ self,
44
+ image
45
+ ) -> np.ndarray:
46
+
47
+ if isinstance(image, str):
48
+ try:
49
+ if (image.startswith("http:")) or (image.startswith("https:")):
50
+ image = requests.get(image).content
51
+ image = io.BytesIO(image)
52
+ image = Image.open(image)
53
+ else:
54
+ image = Image.open(image)
55
+ image = image.convert('RGBA')
56
+ image = np.array(image)
57
+ except FileNotFoundError:
58
+ raise ValueError(f"File {image} not found.")
59
+
60
+ elif isinstance(image, Image.Image):
61
+ image = image.convert('RGBA')
62
+ image = np.array(image)
63
+
64
+ elif not isinstance(image, np.ndarray):
65
+ raise TypeError("Input must be a http/https url, file path, a PIL Image, or a numpy array.")
66
+
67
+ if image.ndim == 2:
68
+ alpha = np.full_like(image, 255, dtype=np.uint8)
69
+ image = np.stack((image, image, image, alpha), axis=-1)
70
+ elif image.ndim == 3:
71
+ if image.shape[2] == 1:
72
+ alpha = np.full_like(image[:, :, 0], 255, dtype=np.uint8)
73
+ image = np.repeat(image, 3, axis=2)
74
+ image = np.dstack((image, alpha))
75
+ elif image.shape[2] == 3:
76
+ alpha = np.full((image.shape[0], image.shape[1]), 255, dtype=np.uint8)
77
+ image = np.dstack((image, alpha))
78
+ elif image.shape[2] != 4:
79
+ raise ValueError("Unsupported number of channels in the numpy array.")
80
+ else:
81
+ raise ValueError("The input image is incorrect")
82
+
83
+ return image
84
+
85
+
86
+ def get_color(
87
+ self,
88
+ quality: int = 10
89
+ ) -> Union[Tuple[int, int, int], Tuple[int, int, int, int]]:
90
+ """
91
+ Get the dominant color.
92
+
93
+ Args:
94
+ quality:
95
+ quality settings, 1 is the highest quality, the bigger
96
+ the number, the faster a color will be returned but
97
+ the greater the likelihood that it will not be the
98
+ visually most dominant color.
99
+
100
+ Returns:
101
+ tuple: (r, g, b) or (r, g, b, percent)
102
+ """
103
+ palette = self.get_palette(5, quality)
104
+ return palette[0]
105
+
106
+
107
+ def get_palette(
108
+ self,
109
+ color_count: int = 10,
110
+ quality: int = 10,
111
+ brightness_filter_low: int = None,
112
+ brightness_filter_high: int = None,
113
+ hue_distance_filter: int = None,
114
+ custom_processing: Callable[[np.ndarray], np.ndarray] = None
115
+ ) -> Union[List[Tuple[int, int, int]], List[Tuple[int, int, int, int]]]:
116
+ """
117
+ Build a color palette. We are using the median cut algorithm to
118
+ cluster similar colors.
119
+
120
+ Args:
121
+ color_count:
122
+ The size of the palette, max number of colors.
123
+ Default is 10.
124
+ quality:
125
+ Quality settings, 1 is the highest quality, the bigger
126
+ the number, the faster the palette generation, but the
127
+ greater the likelihood that colors will be missed.
128
+ Default is 10.
129
+ brightness_filter_low (int):
130
+ Remove colors that are too dark based on their brightness value.
131
+ Default is None.
132
+ brightness_filter_high (int):
133
+ Remove colors that are too light based on their brightness value.
134
+ Default is None.
135
+ hue_distance_filter (int):
136
+ Remove colors that are too close to each other for the specified hue.
137
+ This keeps the colors looking fairly distinct.
138
+ Default is None.
139
+ custom_processing:
140
+ Customize pixel processing functions.
141
+ Returns:
142
+ list: a list of tuple in the form (r, g, b) or (r, g, b, percent)
143
+
144
+ The returned list of RGB values will always have a length value that
145
+ matches the `color_count` argument. If colors are filtered, then the
146
+ list is padded with black (e.g. 0,0,0) RGB entries. This ensures that
147
+ the list length always matches the requested color count.
148
+
149
+ If the `brightness_filter_x` values are specified, each color in the
150
+ returned array is converted to a brightness value (R+G+B=brightness, 0-765)
151
+ and will be filtered out based on the high / low filter arguments.
152
+
153
+ If the `hue_distance_filter` is specified, each color in the returned array
154
+ is converted to a hue value and will be filtered out based on the distance
155
+ between in and the next color.
156
+ """
157
+ # validations.
158
+ if custom_processing is None:
159
+ custom_processing = PixelsProcessor.get_valid_pixels
160
+
161
+ if (not isinstance(color_count, int)) \
162
+ or (color_count < 0):
163
+ color_count = 10
164
+
165
+ image_array = np.reshape(self.image, (-1, 4))
166
+ image_array = image_array[::quality, :]
167
+
168
+ valid_pixels = custom_processing(image_array)
169
+
170
+ # Send array to quantize function which clusters values using median cut algorithm
171
+ cmap = MMCQ.quantize(valid_pixels, color_count + 1)
172
+ cmap.return_percent = self.return_percent
173
+ result = cmap.palette
174
+
175
+ # if brightness filters specified, then apply them.
176
+ if (brightness_filter_low is not None) or (brightness_filter_high is not None):
177
+ result = self.filter_colors_by_brightness(result, low=brightness_filter_low, high=brightness_filter_high)
178
+
179
+ if (hue_distance_filter is not None):
180
+ result = self.filter_colors_by_hue(result, min_distance=hue_distance_filter)
181
+
182
+ # ensure number of palette entries returned matches requested color count.
183
+ palette_len:int = len(result or [])
184
+ if palette_len < color_count:
185
+ for i in range(palette_len, color_count):
186
+ result.append([0,0,0])
187
+
188
+ # return result to caller.
189
+ return result
190
+
191
+
192
+ def rgb_brightness(
193
+ self,
194
+ rgb: List[RGB]
195
+ ) -> int:
196
+ """
197
+ Return brightness as sum of R+G+B (0-765).
198
+ """
199
+ r, g, b = rgb
200
+ return r + g + b
201
+
202
+
203
+ def filter_colors_by_brightness(
204
+ self,
205
+ colors: List[RGB],
206
+ low: int = 200,
207
+ high: int = 600
208
+ ) -> List[RGB]:
209
+ """
210
+ Keep only colors with brightness between [low, high].
211
+ Brightness is R+G+B, range 0-765.
212
+ """
213
+ # validations.
214
+ if low is None:
215
+ low = 0
216
+ if high is None:
217
+ high = 765
218
+
219
+ # validations - brightness filter can only be between 0-765 (R+G+B=brightness).
220
+ if isinstance(low, int):
221
+ if low < 0:
222
+ low = 0
223
+ if isinstance(high, int):
224
+ if high > 765:
225
+ high = 765
226
+
227
+ # process brightness filter.
228
+ return [c for c in colors if low <= self.rgb_brightness(c) <= high]
229
+
230
+
231
+ def rgb_to_hue(
232
+ self,
233
+ rgb: RGB
234
+ ) -> float:
235
+ """
236
+ Convert RGB (0-255 each) to hue in range 0-360 degrees.
237
+ """
238
+ r, g, b = [x / 255.0 for x in rgb]
239
+ h, _, _ = colorsys.rgb_to_hsv(r, g, b) # h in [0.0, 1.0)
240
+ return h * 360
241
+
242
+
243
+ def circular_distance(
244
+ self,
245
+ h1: float,
246
+ h2: float
247
+ ) -> float:
248
+ """
249
+ Return the circular distance between two hue angles (0-360).
250
+ """
251
+ diff = abs(h1 - h2)
252
+ return min(diff, 360 - diff)
253
+
254
+
255
+ def filter_colors_by_hue(
256
+ self,
257
+ colors: List[RGB],
258
+ min_distance: float = 20.0
259
+ ) -> List[RGB]:
260
+ """
261
+ Keep only colors whose hue is at least `min_distance` apart.
262
+ Uses circular distance around the color wheel.
263
+ """
264
+ # validations.
265
+ if min_distance is None:
266
+ min_distance = 20
267
+
268
+ # validations - hue min distance filter can only be between 0-255.
269
+ if isinstance(min_distance, int):
270
+ if min_distance < 0:
271
+ min_distance = 0
272
+ if min_distance > 255:
273
+ min_distance = 255
274
+
275
+ kept: List[ColorThiefFast.RGB] = []
276
+ kept_hues: List[float] = []
277
+
278
+ for c in colors:
279
+ hue = self.rgb_to_hue(c)
280
+ if all(self.circular_distance(hue, kh) >= min_distance for kh in kept_hues):
281
+ kept.append(c)
282
+ kept_hues.append(hue)
283
+ return kept
284
+
285
+
286
+ class PixelsProcessor:
287
+ def get_valid_pixels(
288
+ image_array: np.ndarray
289
+ ) -> np.ndarray:
290
+ """
291
+ Default method to obtain valid pixels.
292
+ """
293
+ condition = ((image_array[..., 3] >= 125) &
294
+ ~((image_array[..., 0] > 250) & (image_array[..., 1] > 250) & (image_array[..., 2] > 250)))
295
+
296
+ valid_pixels = image_array[condition][:, :3]
297
+ return valid_pixels
298
+
299
+
300
+ class MMCQ:
301
+ """
302
+ Basic Python port of the MMCQ (modified median cut quantization)
303
+ algorithm from the Leptonica library (http://www.leptonica.com/).
304
+ """
305
+ SIGBITS = 5
306
+ RSHIFT = 8 - SIGBITS
307
+ MAX_ITERATION = 1000
308
+ FRACT_BY_POPULATIONS = 0.75
309
+
310
+ @staticmethod
311
+ def get_color_index(r, g, b):
312
+ """
313
+ Each quantified (r,g,b) will calculate a unique index value.
314
+ """
315
+ return (r << (2 * MMCQ.SIGBITS)) + (g << MMCQ.SIGBITS) + b
316
+
317
+ @staticmethod
318
+ def get_histo(
319
+ pixels: np.ndarray
320
+ ) -> Dict[int, int]:
321
+ """
322
+ Calculate the number of pixels in each quantized region of the color space,
323
+ and save it in histo dict.
324
+ """
325
+ pixels = pixels.astype(np.uint32)
326
+
327
+ r = np.right_shift(pixels[:, 0], MMCQ.RSHIFT)
328
+ g = np.right_shift(pixels[:, 1], MMCQ.RSHIFT)
329
+ b = np.right_shift(pixels[:, 2], MMCQ.RSHIFT)
330
+ color_index_array = MMCQ.get_color_index(r, g, b)
331
+
332
+ unique_indices, counts = np.unique(color_index_array, return_counts=True)
333
+ histo = {i: int(count) for i, count in zip(unique_indices, counts) if count != 0}
334
+ return histo
335
+
336
+ @staticmethod
337
+ def vbox_from_pixels(pixels: np.ndarray, histo: Dict[int, int]) -> "VBox":
338
+ rval = np.right_shift(pixels[:, 0], MMCQ.RSHIFT)
339
+ gval = np.right_shift(pixels[:, 1], MMCQ.RSHIFT)
340
+ bval = np.right_shift(pixels[:, 2], MMCQ.RSHIFT)
341
+ rmin = rval.min()
342
+ rmax = rval.max()
343
+ gmin = gval.min()
344
+ gmax = gval.max()
345
+ bmin = bval.min()
346
+ bmax = bval.max()
347
+ return VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo, pixels.shape[0])
348
+
349
+ @staticmethod
350
+ def median_cut_apply(histo, vbox: "VBox"):
351
+ if not vbox.count:
352
+ return (None, None)
353
+
354
+ rw = vbox.r2 - vbox.r1 + 1
355
+ gw = vbox.g2 - vbox.g1 + 1
356
+ bw = vbox.b2 - vbox.b1 + 1
357
+ maxw = max([rw, gw, bw])
358
+
359
+ # only one pixel, no split
360
+ if vbox.count == 1:
361
+ return (vbox.copy, None)
362
+
363
+ # Find the partial sum arrays along the selected axis.
364
+ partial_sum = {}
365
+ lookahead_sum = {}
366
+
367
+ if maxw == rw:
368
+ do_cut_color = 'r'
369
+ column_num = 0
370
+ elif maxw == gw:
371
+ do_cut_color = 'g'
372
+ column_num = 1
373
+ else:
374
+ do_cut_color = 'b'
375
+ column_num = 2
376
+
377
+ rgb_array, color_index_array = vbox.get_rgb_array_and_color_index_array()
378
+ color_num_list = [histo.get(index, 0) for index in color_index_array]
379
+ unique_values, indices = np.unique(rgb_array[:, column_num], return_inverse=True)
380
+ color_sum_array = np.bincount(indices, weights=color_num_list)
381
+
382
+ total_array = np.cumsum(color_sum_array, dtype=np.int32)
383
+ for i, t in zip(unique_values, total_array):
384
+ partial_sum[i] = t
385
+
386
+ total = total_array[-1]
387
+ for i, d in partial_sum.items():
388
+ lookahead_sum[i] = total - d
389
+
390
+ # determine the cut planes
391
+ dim1 = do_cut_color + '1'
392
+ dim2 = do_cut_color + '2'
393
+ dim1_val = getattr(vbox, dim1)
394
+ dim2_val = getattr(vbox, dim2)
395
+ for i in range(dim1_val, dim2_val + 1):
396
+ if partial_sum[i] > (total / 2):
397
+ vbox1 = vbox.copy
398
+ vbox2 = vbox.copy
399
+ left = i - dim1_val
400
+ right = dim2_val - i
401
+ if left <= right:
402
+ d2 = min([dim2_val - 1, int(i + right / 2)])
403
+ else:
404
+ d2 = max([dim1_val, int(i - 1 - left / 2)])
405
+
406
+ # avoid 0-count boxes
407
+ while not partial_sum.get(d2, False):
408
+ d2 += 1
409
+ count2 = lookahead_sum.get(d2)
410
+ while not count2 and partial_sum.get(d2 - 1, False):
411
+ d2 -= 1
412
+ count2 = lookahead_sum.get(d2)
413
+
414
+ # set dimensions
415
+ setattr(vbox1, dim2, d2)
416
+ setattr(vbox2, dim1, getattr(vbox1, dim2) + 1)
417
+ return (vbox1, vbox2)
418
+ return (None, None)
419
+
420
+ @staticmethod
421
+ def quantize(pixels: np.ndarray, max_color: int):
422
+ """
423
+ Pixels quantize.
424
+
425
+ Args:
426
+ pixels: a array of pixel in the form (r, g, b)
427
+ max_color: max number of colors
428
+
429
+ Returns:
430
+
431
+ """
432
+ if pixels.shape[0] == 0:
433
+ raise Exception('Empty pixels when quantize.')
434
+ if max_color < 2 or max_color > 256:
435
+ raise Exception('Wrong number of max colors when quantize.')
436
+
437
+ histo = MMCQ.get_histo(pixels)
438
+
439
+ # get the beginning vbox from the colors
440
+ vbox = MMCQ.vbox_from_pixels(pixels, histo)
441
+ pq = PQueue(lambda x: x.count)
442
+ pq.push(vbox)
443
+
444
+ # inner function to do the iteration
445
+ def iter_(lh: "PQueue", target: Union[int, float]):
446
+ n_color = 1
447
+ n_iter = 0
448
+ while n_iter < MMCQ.MAX_ITERATION:
449
+ vbox = lh.pop()
450
+ if not vbox.count: # just put it back
451
+ lh.push(vbox)
452
+ n_iter += 1
453
+ continue
454
+
455
+ # do the cut
456
+ vbox1, vbox2 = MMCQ.median_cut_apply(histo, vbox)
457
+ if not vbox1:
458
+ raise Exception("vbox1 not defined; shouldn't happen!")
459
+ lh.push(vbox1)
460
+
461
+ if vbox2: # vbox2 can be null
462
+ lh.push(vbox2)
463
+ n_color += 1
464
+ if n_color >= target:
465
+ return
466
+ if n_iter > MMCQ.MAX_ITERATION:
467
+ return
468
+ n_iter += 1
469
+
470
+ # first set of colors, sorted by population
471
+ iter_(pq, MMCQ.FRACT_BY_POPULATIONS * max_color)
472
+
473
+ # Re-sort by the product of pixel occupancy times the size in color space.
474
+ pq2 = PQueue(lambda x: x.count * x.volume)
475
+ while pq.size():
476
+ pq2.push(pq.pop())
477
+
478
+ # next set - generate the median cuts using the (npix * vol) sorting.
479
+ iter_(pq2, max_color - pq2.size())
480
+
481
+ # calculate the actual colors
482
+ cmap = CMap()
483
+ while pq2.size():
484
+ cmap.push(pq2.pop())
485
+ return cmap
486
+
487
+
488
+ class VBox:
489
+ """
490
+ 3D color space box.
491
+ """
492
+ def __init__(self, r1, r2, g1, g2, b1, b2, histo: Dict[int, int], total_pixel_count: int):
493
+ self.r1 = int(r1)
494
+ self.r2 = int(r2)
495
+ self.g1 = int(g1)
496
+ self.g2 = int(g2)
497
+ self.b1 = int(b1)
498
+ self.b2 = int(b2)
499
+ self.histo = histo
500
+ self.total_pixel_count = total_pixel_count
501
+
502
+ self._rgb_array: Optional[np.ndarray] = None
503
+ self._color_index: Optional[np.ndarray] = None
504
+
505
+ @functools.cached_property
506
+ def volume(self) -> int:
507
+ """
508
+ The volume of pixels in VBox.
509
+ """
510
+ sub_r = self.r2 - self.r1
511
+ sub_g = self.g2 - self.g1
512
+ sub_b = self.b2 - self.b1
513
+ return (sub_r + 1) * (sub_g + 1) * (sub_b + 1)
514
+
515
+ @property
516
+ def copy(self) -> "VBox":
517
+ return VBox(self.r1, self.r2, self.g1, self.g2, self.b1, self.b2, self.histo, self.total_pixel_count)
518
+
519
+ @functools.cached_property
520
+ def avg(self) -> Tuple[int, int, int]:
521
+ """
522
+ Calculate the color avg of VBox.
523
+ """
524
+ mult = 1 << (8 - MMCQ.SIGBITS)
525
+ rgb_array, color_index_array = self.get_rgb_array_and_color_index_array()
526
+ histo_num = np.array([self.histo.get(c, 0) for c in color_index_array], dtype=np.int32)
527
+ r_sum = np.sum(histo_num * (rgb_array[:, 0] + 0.5) * mult)
528
+ g_sum = np.sum(histo_num * (rgb_array[:, 1] + 0.5) * mult)
529
+ b_sum = np.sum(histo_num * (rgb_array[:, 2] + 0.5) * mult)
530
+
531
+ total = histo_num.sum()
532
+ if total:
533
+ r_avg = r_sum / total
534
+ g_avg = g_sum / total
535
+ b_avg = b_sum / total
536
+ else:
537
+ r_avg = mult * (self.r1 + self.r2 + 1) / 2
538
+ g_avg = mult * (self.g1 + self.g2 + 1) / 2
539
+ b_avg = mult * (self.b1 + self.b2 + 1) / 2
540
+
541
+ r_avg = int(np.clip(r_avg, 0, 255))
542
+ g_avg = int(np.clip(g_avg, 0, 255))
543
+ b_avg = int(np.clip(b_avg, 0, 255))
544
+ return r_avg, g_avg, b_avg
545
+
546
+ @functools.cached_property
547
+ def count(self) -> int:
548
+ """
549
+ The number of pixels in VBox.
550
+ """
551
+ rgb_array, color_index_array = self.get_rgb_array_and_color_index_array()
552
+ npix_list = [self.histo.get(i, 0) for i in color_index_array]
553
+ npix = sum(npix_list)
554
+ return npix
555
+
556
+ @functools.cached_property
557
+ def percent(self) -> int:
558
+ """
559
+ The percentage of VBox pixels to all pixels.
560
+ """
561
+ return int(self.count / self.total_pixel_count * 100)
562
+
563
+ def get_rgb_array(self):
564
+ return np.mgrid[self.r1: self.r2 + 1, self.g1: self.g2 + 1, self.b1: self.b2 + 1].T.reshape(-1, 3)
565
+
566
+ def get_rgb_array_and_color_index_array(self):
567
+ if self._rgb_array is None:
568
+ rgb_array = np.mgrid[self.r1: self.r2 + 1, self.g1: self.g2 + 1, self.b1: self.b2 + 1].T.reshape(-1, 3)
569
+ self._rgb_array = rgb_array
570
+ self._color_index_array = MMCQ.get_color_index(rgb_array[:, 0], rgb_array[:, 1], rgb_array[:, 2])
571
+ return self._rgb_array, self._color_index_array
572
+
573
+
574
+ class CMap:
575
+ """
576
+ Color map.
577
+ """
578
+ def __init__(self, return_percent: bool = False):
579
+ self.pqueue: "PQueue" = PQueue(lambda x: x['vbox'].count * x['vbox'].volume)
580
+ self.return_percent = return_percent
581
+
582
+ @property
583
+ def palette(self) -> List:
584
+ if self.return_percent:
585
+ return self.pqueue.map(lambda x: x['color'] + (x['percent'],))
586
+ else:
587
+ return self.pqueue.map(lambda x: x['color'])
588
+
589
+ def push(self, vbox: "VBox"):
590
+ self.pqueue.push({
591
+ 'vbox': vbox,
592
+ 'color': vbox.avg,
593
+ 'percent': vbox.percent,
594
+ })
595
+
596
+ def size(self):
597
+ return self.pqueue.size()
598
+
599
+
600
+ class PQueue:
601
+ """
602
+ Simple priority queue.
603
+ """
604
+ def __init__(self, sort_key: Callable):
605
+ self.sort_key = sort_key
606
+ self.contents: List = []
607
+ self._sorted = False
608
+
609
+ def sort(self):
610
+ self.contents.sort(key=self.sort_key)
611
+ self._sorted = True
612
+
613
+ def push(self, o):
614
+ self.contents.append(o)
615
+ self._sorted = False
616
+
617
+ def pop(self):
618
+ if not self._sorted:
619
+ self.sort()
620
+ return self.contents.pop()
621
+
622
+ def size(self) -> int:
623
+ return len(self.contents)
624
+
625
+ def map(self, f: Callable) -> List:
626
+ return list(map(f, self.contents))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spotifywebapipython
3
- Version: 1.0.243
3
+ Version: 1.0.251
4
4
  Summary: Spotify Web API Python3 Library
5
5
  Author: Todd Lucas
6
6
  Author-email: <thlucas@yahoo.com>
@@ -22,8 +22,9 @@ Requires-Python: >3.11.0
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: lxml >=5.2.0
25
+ Requires-Dist: numpy >=2.3.2
25
26
  Requires-Dist: oauthlib >=3.2.2
26
- Requires-Dist: pillow >=10.3.0
27
+ Requires-Dist: pillow >=11.3.0
27
28
  Requires-Dist: platformdirs >=4.1.0
28
29
  Requires-Dist: PyChromecast >=14.0.7
29
30
  Requires-Dist: pyotp >=2.9.0