spotifywebapiPython 1.0.242__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.
- spotifywebapipython/__init__.py +35 -0
- spotifywebapipython/const.py +114 -0
- spotifywebapipython/docs/__init__.html +2156 -0
- spotifywebapipython/docs/codecopy.js +70 -0
- spotifywebapipython/docs/index.html +514 -0
- spotifywebapipython/docs/search.js +46 -0
- spotifywebapipython/docs/spotifywebapipython/const.html +636 -0
- spotifywebapipython/docs/spotifywebapipython/saappmessages.html +540 -0
- spotifywebapipython/docs/spotifywebapipython/sautils.html +620 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +341 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +598 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +612 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +16557 -0
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +518 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +421 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +400 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +635 -0
- spotifywebapipython/docs/spotifywebapipython.ico +0 -0
- spotifywebapipython/docs/spotifywebapipython_OLD.ico +0 -0
- spotifywebapipython/models/__init__.py +121 -0
- spotifywebapipython/models/album.py +170 -0
- spotifywebapipython/models/albumpagesaved.py +85 -0
- spotifywebapipython/models/albumpagesimplified.py +88 -0
- spotifywebapipython/models/albumsaved.py +102 -0
- spotifywebapipython/models/albumsimplified.py +308 -0
- spotifywebapipython/models/artist.py +146 -0
- spotifywebapipython/models/artistinfo.py +282 -0
- spotifywebapipython/models/artistinfotourevent.py +127 -0
- spotifywebapipython/models/artistpage.py +70 -0
- spotifywebapipython/models/artistsimplified.py +163 -0
- spotifywebapipython/models/audiobook.py +87 -0
- spotifywebapipython/models/audiobookpagesimplified.py +88 -0
- spotifywebapipython/models/audiobooksimplified.py +362 -0
- spotifywebapipython/models/audiofeatures.py +333 -0
- spotifywebapipython/models/author.py +67 -0
- spotifywebapipython/models/category.py +168 -0
- spotifywebapipython/models/categorypage.py +70 -0
- spotifywebapipython/models/chapter.py +84 -0
- spotifywebapipython/models/chapterpagesimplified.py +88 -0
- spotifywebapipython/models/chaptersimplified.py +387 -0
- spotifywebapipython/models/context.py +113 -0
- spotifywebapipython/models/copyright.py +79 -0
- spotifywebapipython/models/device.py +301 -0
- spotifywebapipython/models/episode.py +84 -0
- spotifywebapipython/models/episodepagesaved.py +85 -0
- spotifywebapipython/models/episodepagesimplified.py +88 -0
- spotifywebapipython/models/episodesaved.py +99 -0
- spotifywebapipython/models/episodesimplified.py +370 -0
- spotifywebapipython/models/explicitcontent.py +80 -0
- spotifywebapipython/models/externalids.py +91 -0
- spotifywebapipython/models/externalurls.py +104 -0
- spotifywebapipython/models/followers.py +81 -0
- spotifywebapipython/models/imageobject.py +193 -0
- spotifywebapipython/models/imagevibrantcolors.py +182 -0
- spotifywebapipython/models/linkedfrom.py +165 -0
- spotifywebapipython/models/narrator.py +67 -0
- spotifywebapipython/models/owner.py +162 -0
- spotifywebapipython/models/pageobject.py +343 -0
- spotifywebapipython/models/playeractions.py +196 -0
- spotifywebapipython/models/playerlastplayedinfo.py +334 -0
- spotifywebapipython/models/playerplaystate.py +467 -0
- spotifywebapipython/models/playerqueueinfo.py +207 -0
- spotifywebapipython/models/playhistory.py +136 -0
- spotifywebapipython/models/playhistorypage.py +85 -0
- spotifywebapipython/models/playlist.py +125 -0
- spotifywebapipython/models/playlistpage.py +85 -0
- spotifywebapipython/models/playlistpagesimplified.py +107 -0
- spotifywebapipython/models/playlistsimplified.py +334 -0
- spotifywebapipython/models/playlisttrack.py +143 -0
- spotifywebapipython/models/playlisttracksummary.py +80 -0
- spotifywebapipython/models/recommendationseed.py +135 -0
- spotifywebapipython/models/restrictions.py +86 -0
- spotifywebapipython/models/resumepoint.py +79 -0
- spotifywebapipython/models/searchresponse.py +391 -0
- spotifywebapipython/models/searchresultbase.py +299 -0
- spotifywebapipython/models/show.py +87 -0
- spotifywebapipython/models/showpagesaved.py +85 -0
- spotifywebapipython/models/showpagesimplified.py +88 -0
- spotifywebapipython/models/showsaved.py +99 -0
- spotifywebapipython/models/showsimplified.py +320 -0
- spotifywebapipython/models/spotifyconnectdevice.py +321 -0
- spotifywebapipython/models/spotifyconnectdevices.py +441 -0
- spotifywebapipython/models/track.py +142 -0
- spotifywebapipython/models/trackpage.py +110 -0
- spotifywebapipython/models/trackpagesaved.py +85 -0
- spotifywebapipython/models/trackpagesimplified.py +88 -0
- spotifywebapipython/models/trackrecommendations.py +88 -0
- spotifywebapipython/models/tracksaved.py +99 -0
- spotifywebapipython/models/tracksimplified.py +398 -0
- spotifywebapipython/models/userprofile.py +184 -0
- spotifywebapipython/models/userprofilesimplified.py +206 -0
- spotifywebapipython/models/zeroconfdiscoveryresult.py +592 -0
- spotifywebapipython/models/zeroconfproperty.py +93 -0
- spotifywebapipython/oauthcli/__init__.py +7 -0
- spotifywebapipython/oauthcli/authclient.py +1229 -0
- spotifywebapipython/saappmessages.py +86 -0
- spotifywebapipython/sautils.py +285 -0
- spotifywebapipython/spotifyapierror.py +66 -0
- spotifywebapipython/spotifyapimessage.py +269 -0
- spotifywebapipython/spotifyauthtoken.py +264 -0
- spotifywebapipython/spotifyclient.py +17885 -0
- spotifywebapipython/spotifyconnect/__init__.py +19 -0
- spotifywebapipython/spotifyconnect/spotifyconnectdeviceeventargs.py +67 -0
- spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +2564 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +467 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +701 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +359 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonelistener.py +212 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +20 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
- spotifywebapipython/spotifydiscovery.py +473 -0
- spotifywebapipython/spotifymediatypes.py +53 -0
- spotifywebapipython/spotifywebapiauthenticationerror.py +122 -0
- spotifywebapipython/spotifywebapierror.py +106 -0
- spotifywebapipython/spotifywebplayertoken.py +743 -0
- spotifywebapipython/vibrant/__init__.py +17 -0
- spotifywebapipython/vibrant/generator.py +261 -0
- spotifywebapipython/vibrant/image.py +100 -0
- spotifywebapipython/vibrant/main.py +34 -0
- spotifywebapipython/vibrant/models.py +93 -0
- spotifywebapipython/vibrant/utils.py +56 -0
- spotifywebapipython/zeroconfapi/__init__.py +23 -0
- spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
- spotifywebapipython/zeroconfapi/blobbuilder.py +370 -0
- spotifywebapipython/zeroconfapi/credentials.py +37 -0
- spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +89 -0
- spotifywebapipython/zeroconfapi/helpers.py +64 -0
- spotifywebapipython/zeroconfapi/spotifyzeroconfapierror.py +108 -0
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +1327 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +572 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +109 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
- spotifywebapipython/zeroconfapi/zeroconfresponse.py +195 -0
- spotifywebapipython-1.0.242.dist-info/LICENSE +22 -0
- spotifywebapipython-1.0.242.dist-info/METADATA +219 -0
- spotifywebapipython-1.0.242.dist-info/RECORD +138 -0
- spotifywebapipython-1.0.242.dist-info/WHEEL +5 -0
- spotifywebapipython-1.0.242.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# "vibrant" namespace was imported from "vibrant-python" namespace (https://github.com/totallynotadi/vibrant-python)
|
|
2
|
+
# had to import it, as the "vibrant-python" version has a dependency that was not compatible with current version of Home Assistant:
|
|
3
|
+
# - "vibrant-python==0.1.6 depends on pillow>=10.1.0,<11.0.0"
|
|
4
|
+
|
|
5
|
+
# import all classes from the namespace.
|
|
6
|
+
from .image import VibrantImage
|
|
7
|
+
from .main import Vibrant
|
|
8
|
+
from .models import Palette, Props, Swatch
|
|
9
|
+
from .utils import hsl_to_rgb, rgb_to_hsl
|
|
10
|
+
|
|
11
|
+
# all classes to import when "import *" is specified.
|
|
12
|
+
__all__ = [
|
|
13
|
+
'VibrantImage',
|
|
14
|
+
'Vibrant',
|
|
15
|
+
'Palette',
|
|
16
|
+
'Swatch',
|
|
17
|
+
]
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from .models import GeneratorOpts, Palette, Swatch, hsl_to_rgb
|
|
4
|
+
|
|
5
|
+
generator_opts = GeneratorOpts(
|
|
6
|
+
target_dark_luma=0.26,
|
|
7
|
+
max_dark_luma=0.45,
|
|
8
|
+
min_light_luma=0.55,
|
|
9
|
+
target_light_luma=0.74,
|
|
10
|
+
min_normal_luma=0.3,
|
|
11
|
+
target_normal_luma=0.5,
|
|
12
|
+
max_normal_luma=0.7,
|
|
13
|
+
target_muted_saturation=0.3,
|
|
14
|
+
max_muted_saturation=0.4,
|
|
15
|
+
target_vibrant_saturation=1.0,
|
|
16
|
+
min_vibrant_saturation=0.35,
|
|
17
|
+
weight_saturation=3,
|
|
18
|
+
weight_luma=6.5,
|
|
19
|
+
weight_population=0.5,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_max_population(swatches: List[Swatch]) -> int:
|
|
24
|
+
p = 0
|
|
25
|
+
for swatch in swatches:
|
|
26
|
+
p = max(p, swatch.population)
|
|
27
|
+
return p
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_already_selected(palette: Palette, swatch: Swatch) -> bool:
|
|
31
|
+
return (
|
|
32
|
+
palette.vibrant == swatch
|
|
33
|
+
or palette.dark_vibrant == swatch
|
|
34
|
+
or palette.light_vibrant == swatch
|
|
35
|
+
or palette.muted == swatch
|
|
36
|
+
or palette.dark_muted == swatch
|
|
37
|
+
or palette.light_muted == swatch
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def create_comparison_value(
|
|
42
|
+
saturation: int,
|
|
43
|
+
targetSaturation: int,
|
|
44
|
+
luma: int,
|
|
45
|
+
targetLuma: int,
|
|
46
|
+
population: int,
|
|
47
|
+
maxPopulation: int,
|
|
48
|
+
opts: GeneratorOpts,
|
|
49
|
+
) -> int:
|
|
50
|
+
def weighted_mean(values: List[int]):
|
|
51
|
+
sum = 0
|
|
52
|
+
weight_sum = 0
|
|
53
|
+
for i in range(0, len(values), 2):
|
|
54
|
+
value = values[i]
|
|
55
|
+
weight = values[i + 1]
|
|
56
|
+
sum += value * weight
|
|
57
|
+
weight_sum += weight
|
|
58
|
+
|
|
59
|
+
return sum / weight_sum
|
|
60
|
+
|
|
61
|
+
def invert_diff(value: int, target_value: int) -> int:
|
|
62
|
+
return 1 - abs(value - target_value)
|
|
63
|
+
|
|
64
|
+
return weighted_mean(
|
|
65
|
+
[
|
|
66
|
+
invert_diff(saturation, targetSaturation),
|
|
67
|
+
opts.weight_saturation,
|
|
68
|
+
invert_diff(luma, targetLuma),
|
|
69
|
+
opts.weight_luma,
|
|
70
|
+
population // maxPopulation,
|
|
71
|
+
opts.weight_population,
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def find_color_variation(
|
|
77
|
+
palette: Palette,
|
|
78
|
+
swatches: List[Swatch],
|
|
79
|
+
max_population: int,
|
|
80
|
+
target_luma: int,
|
|
81
|
+
min_luma: int,
|
|
82
|
+
max_luma: int,
|
|
83
|
+
target_saturation: int,
|
|
84
|
+
min_saturation: int,
|
|
85
|
+
max_saturation: int,
|
|
86
|
+
opts: GeneratorOpts,
|
|
87
|
+
) -> Swatch:
|
|
88
|
+
max_swatch: Swatch = None
|
|
89
|
+
max_value = 0
|
|
90
|
+
|
|
91
|
+
for swatch in swatches:
|
|
92
|
+
h, s, l = swatch.hsl
|
|
93
|
+
if (
|
|
94
|
+
s >= min_saturation
|
|
95
|
+
and s <= max_saturation
|
|
96
|
+
and l >= min_luma
|
|
97
|
+
and l <= max_luma
|
|
98
|
+
and not is_already_selected(palette, swatch)
|
|
99
|
+
):
|
|
100
|
+
value = create_comparison_value(
|
|
101
|
+
s,
|
|
102
|
+
target_saturation,
|
|
103
|
+
l,
|
|
104
|
+
target_luma,
|
|
105
|
+
swatch.population,
|
|
106
|
+
max_population,
|
|
107
|
+
opts,
|
|
108
|
+
)
|
|
109
|
+
if max_swatch == None or value > max_value:
|
|
110
|
+
max_swatch = swatch
|
|
111
|
+
max_value = value
|
|
112
|
+
|
|
113
|
+
return max_swatch
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def generate_variation_colors(
|
|
117
|
+
swatches: List[Swatch], maxPopulation: int, opts: GeneratorOpts
|
|
118
|
+
) -> Palette:
|
|
119
|
+
palette: Palette = Palette()
|
|
120
|
+
|
|
121
|
+
palette.vibrant = find_color_variation(
|
|
122
|
+
palette,
|
|
123
|
+
swatches,
|
|
124
|
+
maxPopulation,
|
|
125
|
+
opts.target_normal_luma,
|
|
126
|
+
opts.min_normal_luma,
|
|
127
|
+
opts.max_normal_luma,
|
|
128
|
+
opts.target_vibrant_saturation,
|
|
129
|
+
opts.min_vibrant_saturation,
|
|
130
|
+
1,
|
|
131
|
+
opts,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
palette.light_vibrant = find_color_variation(
|
|
135
|
+
palette,
|
|
136
|
+
swatches,
|
|
137
|
+
maxPopulation,
|
|
138
|
+
opts.target_light_luma,
|
|
139
|
+
opts.min_light_luma,
|
|
140
|
+
1,
|
|
141
|
+
opts.target_vibrant_saturation,
|
|
142
|
+
opts.min_vibrant_saturation,
|
|
143
|
+
1,
|
|
144
|
+
opts,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
palette.dark_vibrant = find_color_variation(
|
|
148
|
+
palette,
|
|
149
|
+
swatches,
|
|
150
|
+
maxPopulation,
|
|
151
|
+
opts.target_dark_luma,
|
|
152
|
+
0,
|
|
153
|
+
opts.max_dark_luma,
|
|
154
|
+
opts.target_vibrant_saturation,
|
|
155
|
+
opts.min_vibrant_saturation,
|
|
156
|
+
1,
|
|
157
|
+
opts,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
palette.muted = find_color_variation(
|
|
161
|
+
palette,
|
|
162
|
+
swatches,
|
|
163
|
+
maxPopulation,
|
|
164
|
+
opts.target_normal_luma,
|
|
165
|
+
opts.min_normal_luma,
|
|
166
|
+
opts.max_normal_luma,
|
|
167
|
+
opts.target_muted_saturation,
|
|
168
|
+
0,
|
|
169
|
+
opts.max_muted_saturation,
|
|
170
|
+
opts,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
palette.light_muted = find_color_variation(
|
|
174
|
+
palette,
|
|
175
|
+
swatches,
|
|
176
|
+
maxPopulation,
|
|
177
|
+
opts.target_light_luma,
|
|
178
|
+
opts.min_light_luma,
|
|
179
|
+
1,
|
|
180
|
+
opts.target_muted_saturation,
|
|
181
|
+
0,
|
|
182
|
+
opts.max_muted_saturation,
|
|
183
|
+
opts,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
palette.dark_muted = find_color_variation(
|
|
187
|
+
palette,
|
|
188
|
+
swatches,
|
|
189
|
+
maxPopulation,
|
|
190
|
+
opts.target_dark_luma,
|
|
191
|
+
0,
|
|
192
|
+
opts.max_dark_luma,
|
|
193
|
+
opts.target_muted_saturation,
|
|
194
|
+
0,
|
|
195
|
+
opts.max_muted_saturation,
|
|
196
|
+
opts,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return palette
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def generate_empty_swatches(palette: Palette, opts: GeneratorOpts) -> None:
|
|
203
|
+
if (
|
|
204
|
+
palette.vibrant is None
|
|
205
|
+
and palette.dark_vibrant is None
|
|
206
|
+
and palette.light_vibrant is None
|
|
207
|
+
):
|
|
208
|
+
if palette.dark_vibrant is None and palette.dark_muted is not None:
|
|
209
|
+
[h, s, l] = palette.dark_muted.hsl
|
|
210
|
+
l = opts.target_dark_luma
|
|
211
|
+
palette.dark_vibrant = Swatch(hsl_to_rgb([h, s, l]), 0)
|
|
212
|
+
|
|
213
|
+
if palette.light_vibrant is None and palette.light_muted is not None:
|
|
214
|
+
[h, s, l] = palette.light_muted.hsl
|
|
215
|
+
l = opts.target_dark_luma
|
|
216
|
+
palette.dark_vibrant = Swatch(hsl_to_rgb([h, s, l]), 0)
|
|
217
|
+
|
|
218
|
+
if palette.vibrant is None and palette.dark_vibrant is not None:
|
|
219
|
+
[h, s, l] = palette.dark_vibrant.hsl
|
|
220
|
+
l = opts.target_normal_luma
|
|
221
|
+
palette.vibrant = Swatch(hsl_to_rgb([h, s, l]), 0)
|
|
222
|
+
elif palette.vibrant is None and palette.light_vibrant is not None:
|
|
223
|
+
[h, s, l] = palette.light_vibrant.hsl
|
|
224
|
+
l = opts.target_normal_luma
|
|
225
|
+
palette.vibrant = Swatch(hsl_to_rgb([h, s, l]), 0)
|
|
226
|
+
|
|
227
|
+
if palette.dark_vibrant is None and palette.vibrant is not None:
|
|
228
|
+
[h, s, l] = palette.vibrant.hsl
|
|
229
|
+
l = opts.target_dark_luma
|
|
230
|
+
palette.dark_vibrant = Swatch(hsl_to_rgb([h, s, l]), 0)
|
|
231
|
+
|
|
232
|
+
if palette.light_vibrant is None and palette.vibrant is not None:
|
|
233
|
+
[h, s, l] = palette.vibrant.hsl
|
|
234
|
+
l = opts.target_light_luma
|
|
235
|
+
palette.light_vibrant = Swatch(hsl_to_rgb([h, s, l]), 0)
|
|
236
|
+
|
|
237
|
+
if palette.muted is None and palette.vibrant is not None:
|
|
238
|
+
[h, s, l] = palette.vibrant.hsl
|
|
239
|
+
l = opts.target_muted_saturation
|
|
240
|
+
palette.muted = Swatch(hsl_to_rgb([h, s, l]), 0)
|
|
241
|
+
|
|
242
|
+
if palette.dark_muted is None and palette.dark_vibrant is not None:
|
|
243
|
+
h, s, l = palette.dark_vibrant.hsl
|
|
244
|
+
l = opts.target_muted_saturation
|
|
245
|
+
palette.dark_muted = Swatch(hsl_to_rgb([h, s, l]), 0)
|
|
246
|
+
|
|
247
|
+
if palette.light_muted is None and palette.light_vibrant is not None:
|
|
248
|
+
h, s, l = palette.light_vibrant.hsl
|
|
249
|
+
l = opts.target_muted_saturation
|
|
250
|
+
palette.light_muted = Swatch(hsl=[h, s, l], population=0)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def generate(swatches: List[Swatch]) -> Palette:
|
|
254
|
+
max_poplation = find_max_population(swatches)
|
|
255
|
+
|
|
256
|
+
palette: Palette = generate_variation_colors(
|
|
257
|
+
swatches, max_poplation, generator_opts
|
|
258
|
+
)
|
|
259
|
+
generate_empty_swatches(palette, generator_opts)
|
|
260
|
+
|
|
261
|
+
return palette
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
from typing import List, Optional, Tuple, Union
|
|
4
|
+
import requests
|
|
5
|
+
from PIL import Image
|
|
6
|
+
from PIL.Image import Image as PILImage
|
|
7
|
+
|
|
8
|
+
from .models import Props, Swatch
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VibrantImage:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
src: Union[
|
|
15
|
+
bytes,
|
|
16
|
+
str,
|
|
17
|
+
io.BytesIO,
|
|
18
|
+
io.BufferedReader,
|
|
19
|
+
PILImage,
|
|
20
|
+
"VibrantImage",
|
|
21
|
+
],
|
|
22
|
+
props: Optional[Props] = Props(),
|
|
23
|
+
) -> None:
|
|
24
|
+
self.image: Image = None
|
|
25
|
+
self.props: Props = props
|
|
26
|
+
|
|
27
|
+
if isinstance(src, str):
|
|
28
|
+
if src.startswith("http"):
|
|
29
|
+
src = requests.get(src).content
|
|
30
|
+
if not os.path.exists(src):
|
|
31
|
+
raise FileNotFoundError("Image doesn't exist at given path - %s." % src)
|
|
32
|
+
|
|
33
|
+
if isinstance(src, bytes):
|
|
34
|
+
src = io.BytesIO(src)
|
|
35
|
+
|
|
36
|
+
if isinstance(src, PILImage):
|
|
37
|
+
self.image = src
|
|
38
|
+
else:
|
|
39
|
+
self.image = Image.open(src)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_url(cls, src: str) -> "VibrantImage":
|
|
43
|
+
src = requests.get(src).content
|
|
44
|
+
src = io.BytesIO(src)
|
|
45
|
+
return cls(Image.open(src))
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_path(cls, src: str) -> "VibrantImage":
|
|
49
|
+
if os.path.exists(src):
|
|
50
|
+
return cls(Image.open(src))
|
|
51
|
+
raise FileNotFoundError("Image doesn't exist at given path - %s." % src)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_bytes(cls, src: bytes) -> "VibrantImage":
|
|
55
|
+
src = io.BytesIO(src)
|
|
56
|
+
return cls(Image.open(src))
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_fp(cls, fp: io.BufferedReader) -> "VibrantImage":
|
|
60
|
+
return cls(Image.open(fp))
|
|
61
|
+
|
|
62
|
+
def scale_down(self):
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
def _swatch_filter(self, swatch: List[int]) -> bool:
|
|
66
|
+
r, g, b = swatch.rgb
|
|
67
|
+
return not (r > 250 and g > 250 and b > 250)
|
|
68
|
+
|
|
69
|
+
def _parse_swatches(
|
|
70
|
+
self,
|
|
71
|
+
raw_swatches: List,
|
|
72
|
+
swatch_populations: List[Tuple[int, int]],
|
|
73
|
+
) -> List[Swatch]:
|
|
74
|
+
swatches = []
|
|
75
|
+
curr_idx = 0
|
|
76
|
+
for idx in range(0, len(raw_swatches), 3):
|
|
77
|
+
if idx + 2 <= (len(raw_swatches) - 1):
|
|
78
|
+
swatches.append(
|
|
79
|
+
Swatch(
|
|
80
|
+
rgb=[
|
|
81
|
+
raw_swatches[idx],
|
|
82
|
+
raw_swatches[idx + 1],
|
|
83
|
+
raw_swatches[idx + 2],
|
|
84
|
+
],
|
|
85
|
+
population=swatch_populations[curr_idx][0],
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
curr_idx += 1
|
|
89
|
+
return swatches
|
|
90
|
+
|
|
91
|
+
def quantize(self) -> List[Swatch]:
|
|
92
|
+
self.image = self.image.quantize(self.props.color_count)
|
|
93
|
+
raw_swatches = self.image.getpalette()
|
|
94
|
+
raw_swatches = list(filter(lambda x: x != 0, raw_swatches))
|
|
95
|
+
swatch_populations = self.image.getcolors(self.props.color_count)
|
|
96
|
+
swatches = self._parse_swatches(
|
|
97
|
+
raw_swatches=raw_swatches,
|
|
98
|
+
swatch_populations=swatch_populations,
|
|
99
|
+
)
|
|
100
|
+
return swatches
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from PIL.Image import Image as PILImage
|
|
5
|
+
|
|
6
|
+
from .generator import generate
|
|
7
|
+
from .image import VibrantImage
|
|
8
|
+
from .models import Palette, Props
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Vibrant:
|
|
12
|
+
props: Props
|
|
13
|
+
|
|
14
|
+
def __init__(self, color_count=64, quality=5) -> None:
|
|
15
|
+
self.props = Props(color_count=color_count, quality=quality)
|
|
16
|
+
|
|
17
|
+
def get_palette(
|
|
18
|
+
self,
|
|
19
|
+
src: Union[
|
|
20
|
+
bytes,
|
|
21
|
+
str,
|
|
22
|
+
io.BytesIO,
|
|
23
|
+
io.BufferedReader,
|
|
24
|
+
PILImage,
|
|
25
|
+
VibrantImage,
|
|
26
|
+
],
|
|
27
|
+
) -> Palette:
|
|
28
|
+
image = src
|
|
29
|
+
if not isinstance(src, VibrantImage):
|
|
30
|
+
image = VibrantImage(src, self.props)
|
|
31
|
+
image.scale_down()
|
|
32
|
+
swatches = image.quantize()
|
|
33
|
+
palette = generate(swatches)
|
|
34
|
+
return palette
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from .utils import hsl_to_rgb, rgb_to_hsl
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Swatch:
|
|
9
|
+
rgb: List[int]
|
|
10
|
+
population: int
|
|
11
|
+
hsl: Optional[List[float]]
|
|
12
|
+
|
|
13
|
+
def __init__(self, rgb=[], population=0, hsl=[]):
|
|
14
|
+
super().__init__()
|
|
15
|
+
if rgb and not hsl:
|
|
16
|
+
object.__setattr__(self, "rgb", rgb)
|
|
17
|
+
object.__setattr__(self, "hsl", rgb_to_hsl(rgb))
|
|
18
|
+
elif hsl and not rgb:
|
|
19
|
+
object.__setattr__(self, "hsl", hsl)
|
|
20
|
+
object.__setattr__(self, "rgb", hsl_to_rgb(hsl))
|
|
21
|
+
object.__setattr__(self, "population", population)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def rgb_hex(self) -> str:
|
|
25
|
+
"""
|
|
26
|
+
The RGB value expressed in displayable hex string format (e.g. "#ff560a").
|
|
27
|
+
"""
|
|
28
|
+
return f"#{self.rgb[0]:02x}{self.rgb[1]:02x}{self.rgb[2]:02x}"
|
|
29
|
+
|
|
30
|
+
def ToDictionary(self) -> dict:
|
|
31
|
+
"""
|
|
32
|
+
Returns a dictionary representation of the class.
|
|
33
|
+
"""
|
|
34
|
+
result:dict = \
|
|
35
|
+
{
|
|
36
|
+
'rgb_hex': self.rgb_hex,
|
|
37
|
+
'rgb': self.rgb,
|
|
38
|
+
'population': self.population,
|
|
39
|
+
'hsl': self.hsl
|
|
40
|
+
}
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
def ToString(self, includeTitle:bool=False) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Returns a displayable string representation of the class.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
includeTitle (str):
|
|
49
|
+
True to include the class name title prefix.
|
|
50
|
+
"""
|
|
51
|
+
msg:str = ''
|
|
52
|
+
if includeTitle:
|
|
53
|
+
msg = 'Swatch:'
|
|
54
|
+
|
|
55
|
+
msg = '%s RGBHex="%s"' % (msg, self.rgb_hex)
|
|
56
|
+
msg = '%s RGB=%s' % (msg, str(self.rgb))
|
|
57
|
+
msg = '%s Population=%s' % (msg, str(self.population))
|
|
58
|
+
msg = '%s HSL=%s' % (msg, str(self.hsl))
|
|
59
|
+
return msg
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Palette:
|
|
64
|
+
vibrant: Swatch = None
|
|
65
|
+
dark_vibrant: Swatch = None
|
|
66
|
+
light_vibrant: Swatch = None
|
|
67
|
+
muted: Swatch = None
|
|
68
|
+
dark_muted: Swatch = None
|
|
69
|
+
light_muted: Swatch = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Props:
|
|
74
|
+
color_count: int = 64
|
|
75
|
+
quality: int = 5
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class GeneratorOpts:
|
|
80
|
+
target_dark_luma: int
|
|
81
|
+
max_dark_luma: int
|
|
82
|
+
min_light_luma: int
|
|
83
|
+
target_light_luma: int
|
|
84
|
+
min_normal_luma: int
|
|
85
|
+
target_normal_luma: int
|
|
86
|
+
max_normal_luma: int
|
|
87
|
+
target_muted_saturation: int
|
|
88
|
+
max_muted_saturation: int
|
|
89
|
+
target_vibrant_saturation: int
|
|
90
|
+
min_vibrant_saturation: int
|
|
91
|
+
weight_saturation: int
|
|
92
|
+
weight_luma: int
|
|
93
|
+
weight_population: int
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from math import ceil
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def rgb_to_hsl(rgb: List[int]) -> List[float]:
|
|
6
|
+
r, g, b = rgb
|
|
7
|
+
r /= 255
|
|
8
|
+
g /= 255
|
|
9
|
+
b /= 255
|
|
10
|
+
min_val = min(r, g, b)
|
|
11
|
+
max_val = max(r, g, b)
|
|
12
|
+
|
|
13
|
+
l = (max_val + min_val) / 2
|
|
14
|
+
|
|
15
|
+
if min_val == max_val:
|
|
16
|
+
s = 0
|
|
17
|
+
elif l <= 0.5:
|
|
18
|
+
s = (max_val - min_val) / (max_val + min_val)
|
|
19
|
+
elif l > 0.5:
|
|
20
|
+
s = (max_val - min_val) / (2.0 - max_val - min_val)
|
|
21
|
+
|
|
22
|
+
if max_val - min_val == 0:
|
|
23
|
+
h = 0
|
|
24
|
+
elif max_val == r:
|
|
25
|
+
h = ((g - b) / (max_val - min_val)) % 6
|
|
26
|
+
elif max_val == g:
|
|
27
|
+
h = (2.0) + (b - r) / (max_val - min_val)
|
|
28
|
+
else:
|
|
29
|
+
h = 4.0 + (r - g) / (max_val - min_val)
|
|
30
|
+
h = h * 60
|
|
31
|
+
h = h + 360 if h < 0 else h
|
|
32
|
+
|
|
33
|
+
return [round(val, 3) for val in (h, s, l)]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def hsl_to_rgb(hsl: List[float]) -> List[int]:
|
|
37
|
+
h, s, l = hsl
|
|
38
|
+
c = (1 - abs((2 * l) - 1)) * s
|
|
39
|
+
x = c * (1 - abs(((h / 60) % 2) - 1))
|
|
40
|
+
m = l - c / 2
|
|
41
|
+
|
|
42
|
+
if 0 <= h < 60:
|
|
43
|
+
r, g, b = c, x, 0
|
|
44
|
+
elif 60 <= h < 120:
|
|
45
|
+
r, g, b = x, c, 0
|
|
46
|
+
elif 120 <= h < 180:
|
|
47
|
+
r, g, b = 0, c, x
|
|
48
|
+
elif 180 <= h < 240:
|
|
49
|
+
r, g, b = 0, x, c
|
|
50
|
+
elif 240 <= h < 300:
|
|
51
|
+
r, g, b = x, 0, c
|
|
52
|
+
elif 300 <= h < 360:
|
|
53
|
+
r, g, b = c, 0, x
|
|
54
|
+
|
|
55
|
+
r, g, b = (r + m) * 255, (g + m) * 255, (b + m) * 255
|
|
56
|
+
return [ceil(val) for val in (r, g, b)]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# import all classes from the namespace.
|
|
2
|
+
from .authenticationtypes import AuthenticationTypes
|
|
3
|
+
from .blobbuilder import BlobBuilder
|
|
4
|
+
from .credentials import Credentials
|
|
5
|
+
from .spotifyzeroconfapierror import SpotifyZeroconfApiError
|
|
6
|
+
from .zeroconfconnect import ZeroconfConnect
|
|
7
|
+
from .zeroconfresponse import ZeroconfResponse
|
|
8
|
+
from .zeroconfgetinfo import ZeroconfGetInfo
|
|
9
|
+
from .zeroconfgetinfoalias import ZeroconfGetInfoAlias
|
|
10
|
+
from .zeroconfgetinfodrmmediaformat import ZeroconfGetInfoDrmMediaFormat
|
|
11
|
+
|
|
12
|
+
# all classes to import when "import *" is specified.
|
|
13
|
+
__all__ = [
|
|
14
|
+
'AuthenticationTypes',
|
|
15
|
+
'BlobBuilder',
|
|
16
|
+
'Credentials',
|
|
17
|
+
'SpotifyZeroconfApiError',
|
|
18
|
+
'ZeroconfConnect',
|
|
19
|
+
'ZeroconfGetInfo',
|
|
20
|
+
'ZeroconfGetInfoAlias',
|
|
21
|
+
'ZeroconfGetInfoDrmMediaFormat',
|
|
22
|
+
'ZeroconfResponse',
|
|
23
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# external package imports.
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
|
|
4
|
+
# our package imports.
|
|
5
|
+
from ..sautils import export
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@export
|
|
9
|
+
class AuthenticationTypes(IntEnum):
|
|
10
|
+
"""
|
|
11
|
+
Represents the type of authentication used.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
USER_PASS = 0
|
|
15
|
+
"""
|
|
16
|
+
UserId and Password (0x00).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
STORED_SPOTIFY_CREDENTIALS = 1
|
|
20
|
+
"""
|
|
21
|
+
Spotify credentials (0x01).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
STORED_FACEBOOK_CREDENTIALS = 2
|
|
25
|
+
"""
|
|
26
|
+
Facebook credentials (0x02).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
SPOTIFY_TOKEN = 3
|
|
30
|
+
"""
|
|
31
|
+
Spotify authorization token (0x03).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
FACEBOOK_TOKEN = 4
|
|
35
|
+
"""
|
|
36
|
+
Facebook authorization token (0x04).
|
|
37
|
+
"""
|
|
38
|
+
|