Index: channels/__init__.py ================================================================== --- channels/__init__.py +++ channels/__init__.py @@ -73,10 +73,11 @@ gtk_list = None # Gtk widget for station treeview gtk_cat = None # Gtk widget for category columns ls = None # ListStore for station treeview rowmap = None # Preserve streams-datamap pix_entry = None # ListStore entry that contains favicon + img_resize = None # Rescale `img` references to icon size # mapping of stream{} data into gtk treeview/treestore representation datamap = [ # coltitle width [ datasrc key, type, renderer, attrs ] [cellrenderer2], ... ["", 20, ["state", str, "pixbuf", {}], ], Index: channels/favicon.py ================================================================== --- channels/favicon.py +++ channels/favicon.py @@ -7,11 +7,11 @@ # { 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 +# version: 2.0 # depends: streamtuner2 >= 2.1.9, python:pil # priority: standard # png: # iVBORw0KGgoAAAANSUhEUgAAABYAAAAWBAMAAAA2mnEIAAAAJ1BMVEUAAACwDw5oKh1RRU5OTSCOTxp0Um9zcyFUhSXsbwChdp/lgCNbrA7VFTQPAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHU # gAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQffBQ4ENQJMtfdfAAAAmUlEQVQY02NgcAECYxBgYODeOXPmTKtVQLCAwXsmjL2YQRPINDNGsFclI7GXQdmzZ87MSoOyI0pnpgHVLAOy1c+c @@ -58,12 +58,16 @@ # 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 +# · 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 # @@ -89,18 +93,19 @@ 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 + # 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): - #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): + 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 @@ -110,16 +115,34 @@ 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) + self.parent.thread( + self.update_rows, + entries=[row], channel=channel, row_i=channel.rowno(), + fresh_homepage=found + ) - # Run through rows[] to update "favicon" from "homepage" or "img", + # Run through rows[] to update "favicon" cachefile from "homepage" or "img", # optionally display new image right away in ListStore - def update_rows(self, entries, pixstore=None, fresh_homepage=False, **x): + # + # · 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: @@ -142,43 +165,40 @@ 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) + 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 + # Update TreeView (single `row_i`, or counted up `i` index) if ok: - self.update_pixstore(row, pixstore, i) + 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 in treeview/liststore - def update_pixstore(self, row, pixstore=None, row_i=None): - log.FAVICON_UPDATE_PIXSTORE(pixstore, row_i) - if not pixstore: + # 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 - # 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) @@ -185,11 +205,11 @@ # 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 + 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"] @@ -255,20 +275,20 @@ return url # Copy banner row["img"] into icons/ directory -def banner_localcopy(url, fn): +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) + 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): @@ -281,11 +301,11 @@ log.FAVICON_IMAGE_TO_PNG(image, image.size, resize) # Resize if resize and image.size[0] > resize: try: - image.thumbnail(resize, Image.ANTIALIAS) + image.thumbnail((resize, resize), Image.ANTIALIAS) except: image = image.resize((resize,resize), Image.ANTIALIAS) # Convert to PNG via string buffer out = BytesIO() Index: st2.py ================================================================== --- st2.py +++ st2.py @@ -8,11 +8,11 @@ # state: beta # author: Mario Salzer # license: Public Domain # url: http://freshcode.club/projects/streamtuner2 # config: -# { type: env, name: http_proxy, description: proxy for HTTP access } +# { type: env, name: HTTP_PROXY, description: proxy for HTTP access } # { type: env, name: XDG_CONFIG_HOME, description: relocates user .config subdirectory } # category: sound # depends: pygtk | gi, threading, requests, pyquery, lxml # id: streamtuner2 # pack: *.py, gtk3.xml.gz, bin, channels/__init__.py, bundle/*.py, CREDITS, help/index.page, @@ -274,14 +274,13 @@ # Play button def on_play_clicked(self, widget, event=None, *args): self.status("Starting player...") channel = self.channel() - pixstore = [channel.ls, channel.pix_entry, channel.rowno()] row = channel.play() self.status("") - [callback(row, pixstore=pixstore, channel=channel) for callback in self.hooks["play"]] + [callback(row, channel=channel) for callback in self.hooks["play"]] # Recording: invoke streamripper for current stream URL def on_record_clicked(self, widget): self.status("Recording station...") row = self.channel().record() @@ -330,11 +329,11 @@ # Menu invocation: refresh favicons for all stations in current streams category def update_favicons(self, widget): if "favicon" in self.features: ch = self.channel() - self.features["favicon"].update_all(entries=ch.stations(), pixstore=[ch.ls, ch.pix_entry, None]) + self.features["favicon"].update_all(entries=ch.stations(), channel=ch) # Save stream to file (.m3u) def save_as(self, widget): row = self.row() default_fn = row["title"] + ".m3u"