1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# encoding: utf-8
# api: streamtuner2
# title: Favicons
# description: Load and display station favicons/logos.
# config:
# { name: load_favicon, type: bool, value: 1, description: "Load favicon instantly when â–¸playing a station.", color: yellow }
# { name: favicon_google_first, type: bool, value: 1, description: "Prefer faster Google favicon to PNG conversion service." }
# { name: favicon_delete_stub , type: bool, value: 1, description: "Don't accept any placeholder favicons." }
# { name: google_homepage, type: bool, value: 0, description: "Google missing station homepages right away." }
# type: feature
# category: ui
# version: 1.9
# depends: streamtuner2 >= 2.1.9, python:pil
# priority: standard
# png:
# iVBORw0KGgoAAAANSUhEUgAAABYAAAAWBAMAAAA2mnEIAAAAJ1BMVEUAAACwDw5oKh1RRU5OTSCOTxp0Um9zcyFUhSXsbwChdp/lgCNbrA7VFTQPAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHU
# gAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQffBQ4ENQJMtfdfAAAAmUlEQVQY02NgcAECYxBgYODeOXPmTKtVQLCAwXsmjL2YQRPINDNGsFclI7GXQdmzZ87MSoOyI0pnpgHVLAOy1c+c
# mTkzeFWioBSUbQZkiy1mcPFpCXUxTksTFEtm8Ojp6OhQVDJWVFJi8DkDBIIgIARhKyKx3c8g2GfOBCKxFeHspg6EmiZFJDbEHB44W4CBwQNor5MSEDAAAGcoaQmD1t8TAAAAAElFTkSuQmCC
#
|
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# encoding: utf-8
# api: streamtuner2
# title: Favicons
# description: Load and display station favicons/logos.
# config:
# { name: load_favicon, type: bool, value: 1, description: "Load favicon instantly when â–¸playing a station.", color: yellow }
# { name: favicon_google_first, type: bool, value: 1, description: "Prefer faster Google favicon to PNG conversion service." }
# { name: favicon_delete_stub , type: bool, value: 1, description: "Don't accept any placeholder favicons." }
# { name: google_homepage, type: bool, value: 0, description: "Google missing station homepages right away." }
# type: feature
# category: ui
# version: 2.0
# depends: streamtuner2 >= 2.1.9, python:pil
# priority: standard
# png:
# iVBORw0KGgoAAAANSUhEUgAAABYAAAAWBAMAAAA2mnEIAAAAJ1BMVEUAAACwDw5oKh1RRU5OTSCOTxp0Um9zcyFUhSXsbwChdp/lgCNbrA7VFTQPAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHU
# gAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQffBQ4ENQJMtfdfAAAAmUlEQVQY02NgcAECYxBgYODeOXPmTKtVQLCAwXsmjL2YQRPINDNGsFclI7GXQdmzZ87MSoOyI0pnpgHVLAOy1c+c
# mTkzeFWioBSUbQZkiy1mcPFpCXUxTksTFEtm8Ojp6OhQVDJWVFJi8DkDBIIgIARhKyKx3c8g2GfOBCKxFeHspg6EmiZFJDbEHB44W4CBwQNor5MSEDAAAGcoaQmD1t8TAAAAAElFTkSuQmCC
#
|
︙ | | | ︙ | |
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
# · GenericChannel presets row["favicon"] with cache image filenames
# in any case. It calls row_to_fn() per prepare_filters hook after
# station list updates.
#
# · uikit.columns() merely checks row["favicon"] for file existence
# when redrawing a station list.
#
# · main calls .update_playing() on hooks["play"],
# or .update_all() per menu command
# Hook up as feature plugin
#
class favicon(object):
|
|
>
>
>
|
>
|
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
# · GenericChannel presets row["favicon"] with cache image filenames
# in any case. It calls row_to_fn() per prepare_filters hook after
# station list updates.
#
# · uikit.columns() merely checks row["favicon"] for file existence
# when redrawing a station list.
#
# · The main window calls .update_playing() on hooks["play"].
# (Which passes the current row{} and row_i index, and its channel
# object for updating the ListStore→pixbuf right away.)
#
# · Main also calls .update_all() wrapper per menu command "Channel ›
# Update Favicons..."
# Hook up as feature plugin
#
class favicon(object):
|
︙ | | | ︙ | |
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
|
# Prepare favicon cache directory
conf.icon_dir = conf.dir + "/icons"
if not os.path.exists(conf.icon_dir):
os.mkdir(conf.icon_dir)
open(icon_dir+"/.nobackup", "a").close()
# Main menu "Update favicons": update favicon cache for complete list of station rows
def update_all(self, *args, **kwargs):
#kwargs[pixstore] = self.parent.channel()._ls, ...
self.parent.thread(self.update_rows, *args, **kwargs)
# Main [â–¸play] event for a single station
def update_playing(self, row, pixstore=None, channel=None, **x):
# Homepage search
if conf.google_homepage and not len(row.get("homepage", "")):
found = google_find_homepage(row)
# Save channel list right away to preserve found homepage URL
if found and conf.auto_save_stations:
channel.save()
else:
found = False
# Favicon only for currently playing station
if conf.load_favicon:
if row.get("homepage") or row.get("img"):
self.parent.thread(self.update_rows, [row], pixstore=pixstore, fresh_homepage=found)
# Run through rows[] to update "favicon" from "homepage" or "img",
# optionally display new image right away in ListStore
def update_rows(self, entries, pixstore=None, fresh_homepage=False, **x):
for i,row in enumerate(entries):
ok = False
# Try just once
if row.get("homepage") in tried_urls:
continue
# Ignore existing ["favicon"] filename
|
|
>
>
<
|
|
>
>
>
|
|
>
|
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
|
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
|
# Prepare favicon cache directory
conf.icon_dir = conf.dir + "/icons"
if not os.path.exists(conf.icon_dir):
os.mkdir(conf.icon_dir)
open(icon_dir+"/.nobackup", "a").close()
# Main menu "Update favicons": update favicon cache for complete list
# of station rows. Just a wrapper now around update_rows(). Expects
# both entries=[] and channel={} argument still.
def update_all(self, *args, **kwargs):
self.parent.thread(self.update_rows, *args, **kwargs)
# Main [â–¸play] event for a single station
def update_playing(self, row, channel=None, **x):
# Homepage search
if conf.google_homepage and not len(row.get("homepage", "")):
found = google_find_homepage(row)
# Save channel list right away to preserve found homepage URL
if found and conf.auto_save_stations:
channel.save()
else:
found = False
# Favicon only for currently playing station
if conf.load_favicon:
if row.get("homepage") or row.get("img"):
self.parent.thread(
self.update_rows,
entries=[row], channel=channel, row_i=channel.rowno(),
fresh_homepage=found
)
# Run through rows[] to update "favicon" cachefile from "homepage" or "img",
# optionally display new image right away in ListStore
#
# · The entries[] list can be a single row. In which case it is accompanied
# by its row_i index.
# · If it's a complete streams list, then the row index will be manually
# counted up.
# · This is needed to update the pixstore. The `channel` reference is used
# for accessing the displayed ListStore, and its `pix_entry` column for
# updates.
#
def update_rows(self, entries, channel=None, row_i=None, fresh_homepage=False, **x):
# Preserve current ListStore object - in case the channel/notebook
# tab gets switched for longer .update_all() invocations.
ch_ls = channel.ls if channel else None
for i,row in enumerate(entries):
ok = False
# Try just once
if row.get("homepage") in tried_urls:
continue
# Ignore existing ["favicon"] filename
|
︙ | | | ︙ | |
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
|
continue
else: # For freshly added ["homepage"] when favicon already
ok = True # exists in cache. Then just update pix store.
# Download custom "img" banner/logo as favicon
elif row.get("img"):
tried_urls.append(row["img"])
ok = banner_localcopy(row["img"], favicon_fn)
# Fetch homepage favicon into local png
elif row.get("homepage"):
tried_urls.append(row["homepage"])
if conf.favicon_google_first:
ok = fav_google_ico2png(row["homepage"], favicon_fn)
else:
ok = fav_from_homepage(row["homepage"], favicon_fn)
# Update TreeView
if ok:
self.update_pixstore(row, pixstore, i)
row["favicon"] = favicon_fn
# catch HTTP Timeouts etc., so update_all() row processing just continues..
except Exception as e:
log.WARN("favicon.update_rows():", e)
pass
# Update favicon in treeview/liststore
def update_pixstore(self, row, pixstore=None, row_i=None):
log.FAVICON_UPDATE_PIXSTORE(pixstore, row_i)
if not pixstore:
return
# Unpack ListStore, pixbuf column no, preset rowno
ls, pix_entry, i = pixstore
# Else use row index from update_all-iteration
if i is None:
i = row_i
# Existing "favicon" cache filename
if row.get("favicon"):
fn = row["favicon"]
else:
fn = row_to_fn(row)
# Update pixbuf in active station liststore
if fn and os.path.exists(fn):
try:
p = gtk.gdk.pixbuf_new_from_file(fn)
ls[i][pix_entry] = p
except Exception as e:
log.ERR("Update_pixstore image", fn, "error:", e)
# Run after any channel .update_streams() to populate row["favicon"]
# from `homepage` or `img` url.
def prepare_filter_favicon(self, row):
|
>
|
|
>
>
|
|
|
|
|
<
<
<
<
<
<
|
|
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
|
continue
else: # For freshly added ["homepage"] when favicon already
ok = True # exists in cache. Then just update pix store.
# Download custom "img" banner/logo as favicon
elif row.get("img"):
tried_urls.append(row["img"])
resize = row.get("img_resize", channel.img_resize)
ok = banner_localcopy(row["img"], favicon_fn, resize)
# Fetch homepage favicon into local png
elif row.get("homepage"):
tried_urls.append(row["homepage"])
if conf.favicon_google_first:
ok = fav_google_ico2png(row["homepage"], favicon_fn)
else:
ok = fav_from_homepage(row["homepage"], favicon_fn)
# Update TreeView (single `row_i`, or counted up `i` index)
if ok:
if row_i is not None: # single row update
i = row_i
self.update_pixstore(row, ch_ls, channel, i)
row["favicon"] = favicon_fn
# catch HTTP Timeouts etc., so update_all() row processing just continues..
except Exception as e:
log.WARN("favicon.update_rows():", e)
pass
# Update favicon pixbuf in treeview/liststore
def update_pixstore(self, row, ls, channel=None, row_i=None):
log.FAVICON_UPDATE_PIXSTORE(channel, ls, row_i)
if not channel or not ls or row_i is None:
return
# Existing "favicon" cache filename
if row.get("favicon"):
fn = row["favicon"]
else:
fn = row_to_fn(row)
# Update pixbuf in active station liststore
if fn and os.path.exists(fn):
try:
p = gtk.gdk.pixbuf_new_from_file(fn)
ls[row_i][channel.pix_entry] = p
except Exception as e:
log.ERR("Update_pixstore image", fn, "error:", e)
# Run after any channel .update_streams() to populate row["favicon"]
# from `homepage` or `img` url.
def prepare_filter_favicon(self, row):
|
︙ | | | ︙ | |
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
|
url = rx_non_wordchr.sub("_", url) # remove any non-word characters
url = "{}/{}.png".format(conf.icon_dir, url) # prefix cache directory
return url
# Copy banner row["img"] into icons/ directory
def banner_localcopy(url, fn):
# Check URL and target filename
if not re.match("^https?://[\w.-]{10}", url):
return False
# Fetch and save
imgdata = ahttp.get(url, binary=1, verify=False)
if imgdata:
return store_image(imgdata, fn)
# Check for valid image binary, possibly convert or resize, then save to cache filename
def store_image(imgdata, fn, resize=None):
# Convert accepted formats -- even PNG for filtering now
if re.match(br'^(.PNG|GIF\d+|.{0,15}(Exif|JFIF)|\x00\x00\x01\x00|.{0,255}<svg[^>]+svg)', imgdata):
try:
# Read from byte/str
image = Image.open(BytesIO(imgdata))
log.FAVICON_IMAGE_TO_PNG(image, image.size, resize)
# Resize
if resize and image.size[0] > resize:
try:
image.thumbnail(resize, Image.ANTIALIAS)
except:
image = image.resize((resize,resize), Image.ANTIALIAS)
# Convert to PNG via string buffer
out = BytesIO()
image.save(out, "PNG", quality=98)
imgdata = out.getvalue()
|
|
|
|
|
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
|
url = rx_non_wordchr.sub("_", url) # remove any non-word characters
url = "{}/{}.png".format(conf.icon_dir, url) # prefix cache directory
return url
# Copy banner row["img"] into icons/ directory
def banner_localcopy(url, fn, resize=None):
# Check URL and target filename
if not re.match("^https?://[\w.-]{10}", url):
return False
# Fetch and save
imgdata = ahttp.get(url, binary=1, verify=False)
if imgdata:
return store_image(imgdata, fn, resize)
# Check for valid image binary, possibly convert or resize, then save to cache filename
def store_image(imgdata, fn, resize=None):
# Convert accepted formats -- even PNG for filtering now
if re.match(br'^(.PNG|GIF\d+|.{0,15}(Exif|JFIF)|\x00\x00\x01\x00|.{0,255}<svg[^>]+svg)', imgdata):
try:
# Read from byte/str
image = Image.open(BytesIO(imgdata))
log.FAVICON_IMAGE_TO_PNG(image, image.size, resize)
# Resize
if resize and image.size[0] > resize:
try:
image.thumbnail((resize, resize), Image.ANTIALIAS)
except:
image = image.resize((resize,resize), Image.ANTIALIAS)
# Convert to PNG via string buffer
out = BytesIO()
image.save(out, "PNG", quality=98)
imgdata = out.getvalue()
|
︙ | | | ︙ | |