Index: _package.epm ================================================================== --- _package.epm +++ _package.epm @@ -31,15 +31,15 @@ d 755 root root /usr/share/streamtuner2 - f 644 root root /usr/share/streamtuner2/streamtuner2.png ./streamtuner2.png f 644 root root /usr/share/pixmaps/streamtuner2.png ./logo.png f 644 root root /usr/share/streamtuner2/gtk2.xml ./gtk2.xml f 644 root root /usr/share/streamtuner2/gtk3.xml ./gtk3.xml -f 644 root root /usr/share/streamtuner2/pson.py ./pson.py +#f 644 root root /usr/share/streamtuner2/pson.py ./pson.py #f 644 root root /usr/share/streamtuner2/processing.py ./processing.py f 644 root root /usr/share/streamtuner2/action.py ./action.py f 644 root root /usr/share/streamtuner2/config.py ./config.py -f 644 root root /usr/share/streamtuner2/http.py ./http.py +f 644 root root /usr/share/streamtuner2/ahttp.py ./ahttp.py f 644 root root /usr/share/streamtuner2/cli.py ./cli.py f 644 root root /usr/share/streamtuner2/mygtk.py ./mygtk.py f 644 root root /usr/share/streamtuner2/favicon.py ./favicon.py f 644 root root /usr/share/streamtuner2/kronos.py ./kronos.py f 644 root root /usr/share/streamtuner2/pq.py ./pq.py Index: action.py ================================================================== --- action.py +++ action.py @@ -21,11 +21,11 @@ # import re import os -import http +import ahttp as http from config import conf, __print__, dbg import platform main = None @@ -45,22 +45,22 @@ # web @staticmethod def browser(url): - __print__( conf.browser ) + __print__( dbg.CONF, conf.browser ) action.run(conf.browser + " " + action.quote(url)) # os shell cmd escaping @staticmethod def quote(s): if conf.windows: - return s # should actually be "\\\"%s\\\"" % s + return str(s) # should actually be "\\\"%s\\\"" % s else: - return "%r" % s + return "%r" % str(s) # calls player for stream url and format @staticmethod def play(url, audioformat="audio/mp3", listformat="text/x-href"): @@ -68,11 +68,11 @@ url = action.url(url, listformat) if (audioformat): if audioformat == "audio/mpeg": audioformat = "audio/mp3" # internally we use the more user-friendly moniker cmd = conf.play.get(audioformat, conf.play.get("*/*", "vlc %u")) - __print__( "play", url, cmd ) + __print__( dbg.PROC,"play", url, cmd ) try: action.run( action.interpol(cmd, url) ) except: pass @@ -79,19 +79,19 @@ # exec wrapper @staticmethod def run(cmd): if conf.windows: - os.system("start \"%s\"") - else: + os.system("start \"%s\"") + else: os.system(cmd + " &") # streamripper @staticmethod def record(url, audioformat="audio/mp3", listformat="text/x-href", append="", row={}): - __print__( "record", url ) + __print__( dbg.PROC, "record", url ) cmd = conf.record.get(audioformat, conf.record.get("*/*", None)) try: action.run( action.interpol(cmd, url, row) + append ) except: pass @@ -189,11 +189,11 @@ # download a .pls resource and extract urls @staticmethod def pls(url): text = http.get(url) - __print__( "pls_text=", text ) + __print__( dbg.DATA, "pls_text=", text ) return re.findall("\s*File\d*\s*=\s*(\w+://[^\s]+)", text, re.I) # currently misses out on the titles # get a single direct ICY stream url (extract either from PLS or M3U) @staticmethod @@ -224,11 +224,11 @@ stream_id = stream_id and stream_id.group(1) or "XXXXXX" try: channelname = main.current_channel except: channelname = "unknown" - return (conf.tmp + os.sep + "streamtuner2."+channelname+"."+stream_id+".m3u", len(stream_id) > 3 and stream_id != "XXXXXX") + return (str(conf.tmp) + os.sep + "streamtuner2."+channelname+"."+stream_id+".m3u", len(stream_id) > 3 and stream_id != "XXXXXX") # check if there are any urls in a given file @staticmethod def has_urls(tmp_fn): if os.path.exists(tmp_fn): @@ -244,13 +244,13 @@ # does it already exist? if tmp_fn and unique and conf.reuse_m3u and action.has_urls(tmp_fn): return tmp_fn # download PLS - __print__( "pls=",pls ) + __print__( dbg.DATA, "pls=",pls ) url_list = action.extract_urls(pls) - __print__( "urls=", url_list ) + __print__( dbg.DATA, "urls=", url_list ) # output URL list to temporary .m3u file if (len(url_list)): #tmp_fn = f = open(tmp_fn, "w") @@ -258,11 +258,11 @@ f.write("\n".join(url_list) + "\n") f.close() # return path/name of temporary file return tmp_fn else: - __print__( "error, there were no URLs in ", pls ) + __print__( dbg.ERR, "error, there were no URLs in ", pls ) raise "Empty PLS" # open help browser @staticmethod def help(*args): ADDED ahttp.py Index: ahttp.py ================================================================== --- ahttp.py +++ ahttp.py @@ -0,0 +1,213 @@ +# +# encoding: UTF-8 +# api: streamtuner2 +# type: functions +# title: http download / methods +# description: http utility +# version: 1.4 +# +# Provides a http GET method with gtk.statusbar() callback. +# And a function to add trailings slashes on http URLs. +# +# + + +from compat2and3 import urllib2, urlencode, urlparse, cookielib, StringIO, xrange, PY3 +from gzip import GzipFile +from config import conf, __print__, dbg + + +#-- url download --------------------------------------------- + + + +#-- chains to progress meter and status bar in main window +feedback = None + +# sets either text or percentage, so may take two parameters +def progress_feedback(*args): + + # use reset values if none given + if not args: + args = ["", 1.0] + + # send to main win + if feedback: + try: [feedback(d) for d in args] + except: pass + + + + +#-- GET +def get(url, maxsize=1<<19, feedback="old"): + __print__( dbg.HTTP, "GET", url) + + # statusbar info + progress_feedback(url, 0.0) + + # read + content = "" + f = urllib2.urlopen(url) + max = 222000 # mostly it's 200K, but we don't get any real information + read_size = 1 + + # multiple steps + while (read_size and len(content) < maxsize): + + # partial read + add = f.read(8192) + content = content + add + read_size = len(add) + + # set progress meter + progress_feedback(float(len(content)) / float(max)) + + # done + + # clean statusbar + progress_feedback() + + # fin + __print__( dbg.INFO, "Content-Length", len(content) ) + return content + + + + + +#-- fix invalid URLs +def fix_url(url): + if url is None: + url = "" + if len(url): + # remove whitespace + url = url.strip() + # add scheme + if (url.find("://") < 0): + url = "http://" + url + # add mandatory path + if (url.find("/", 10) < 0): + url = url + "/" + return url + + + + +# default HTTP headers for AJAX/POST request +default_headers = { + "User-Agent": "streamtuner2/2.1 (X11; U; Linux AMD64; en; rv:1.5.0.1) like WinAmp/2.1 but not like Googlebot/2.1", #"Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6", + "Accept": "*/*;q=0.5, audio/*, url/*", + "Accept-Language": "en-US,en,de,es,fr,it,*;q=0.1", + "Accept-Encoding": "gzip,deflate", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.1", + "Keep-Alive": "115", + "Connection": "keep-alive", + #"Content-Length", "56", + #"Cookie": "s_pers=%20s_getnr%3D1278607170446-Repeat%7C1341679170446%3B%20s_nrgvo%3DRepeat%7C1341679170447%3B; s_sess=%20s_cc%3Dtrue%3B%20s_sq%3Daolshtcst%252Caolsvc%253D%252526pid%25253Dsht%25252520%2525253A%25252520SHOUTcast%25252520Radio%25252520%2525257C%25252520Search%25252520Results%252526pidt%25253D1%252526oid%25253Dfunctiononclick%25252528event%25252529%2525257BshowMoreGenre%25252528%25252529%2525253B%2525257D%252526oidt%25253D2%252526ot%25253DDIV%3B; aolDemoChecked=1.849061", + "Pragma": "no-cache", + "Cache-Control": "no-cache", +} + + + +# simulate ajax calls +def ajax(url, post, referer=""): + + # request + headers = default_headers + headers.update({ + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "X-Requested-With": "XMLHttpRequest", + "Referer": (referer if referer else url), + }) + if type(post) == dict: + post = urlencode(post) + request = urllib2.Request(url, post, headers) + + # open url + __print__( dbg.INFO, vars(request) ) + progress_feedback(url, 0.2) + r = urllib2.urlopen(request) + + # get data + __print__( dbg.HTTP, r.info() ) + progress_feedback(0.5) + data = r.read() + progress_feedback() + return data + + + +# http://techknack.net/python-urllib2-handlers/ +class ContentEncodingProcessor(urllib2.BaseHandler): + """A handler to add gzip capabilities to urllib2 requests """ + + # add headers to requests + def http_request(self, req): + req.add_header("Accept-Encoding", "gzip, deflate") + return req + + # decode + def http_response(self, req, resp): + old_resp = resp + # gzip + if resp.headers.get("content-encoding") == "gzip": + gz = GzipFile( + fileobj=StringIO(resp.read()), + mode="r" + ) + resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code) + resp.msg = old_resp.msg + # deflate + if resp.headers.get("content-encoding") == "deflate": + gz = StringIO( deflate(resp.read()) ) + resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code) # 'class to add info() and geturl() methods to an open file.' + resp.msg = old_resp.msg + return resp + +# deflate support +import zlib +def deflate(data): # zlib only provides the zlib compress format, not the deflate format; + try: # so on top of all there's this workaround: + return zlib.decompress(data, -zlib.MAX_WBITS) + except zlib.error: + return zlib.decompress(data) + + + + + + + +#-- init for later use +if urllib2: + + # config 1 + handlers = [None, None, None] + + # base + handlers[0] = urllib2.HTTPHandler() + if conf.debug: + handlers[0].set_http_debuglevel(3) + + # content-encoding + handlers[1] = ContentEncodingProcessor() + + # store cookies at runtime + cj = cookielib.CookieJar() + handlers[2] = urllib2.HTTPCookieProcessor( cj ) + + # inject into urllib2 + urllib2.install_opener( urllib2.build_opener(*handlers) ) + + + + +# alternative function names +AJAX=ajax +POST=ajax +GET=get +URL=fix_url + + Index: channels/_generic.py ================================================================== --- channels/_generic.py +++ channels/_generic.py @@ -21,11 +21,11 @@ import gtk from mygtk import mygtk from config import conf, __print__, dbg -import http +import ahttp as http import action import favicon import os.path import xml.sax.saxutils import re @@ -70,12 +70,12 @@ # mapping of stream{} data into gtk treeview/treestore representation datamap = [ # coltitle width [ datasrc key, type, renderer, attrs ] [cellrenderer2], ... ["", 20, ["state", str, "pixbuf", {}], ], ["Genre", 65, ['genre', str, "t", {}], ], - ["Station Title",275,["title", str, "text", {"strikethrough":11, "cell-background":12, "cell-background-set":13}], ["favicon",gtk.gdk.Pixbuf,"pixbuf",{"width":20}], ], - ["Now Playing",185, ["playing", str, "text", {"strikethrough":11}], ], + ["Station Title",275,["title", str, "text", {"strikethrough":11, "cell-background":12, "cell-background-set":13}], ["favicon", gtk.gdk.Pixbuf, "pixbuf", {}], ], + ["Now Playing",185, ["playing", str, "text", {"strikethrough":11}], ], #{"width":20} ["Listeners", 45, ["listeners", int, "t", {"strikethrough":11}], ], #["Max", 45, ["max", int, "t", {}], ], ["Bitrate", 35, ["bitrate", int, "t", {}], ], ["Homepage", 160, ["homepage", str, "t", {"underline":10}], ], [False, 25, ["url", str, "t", {"strikethrough":11}], ], @@ -147,17 +147,17 @@ # category tree self.display_categories() #mygtk.tree(self.gtk_cat, self.categories, title="Category", icon=gtk.STOCK_OPEN); # update column names - for field,title in self.titles.iteritems(): + for field,title in list(self.titles.items()): self.update_datamap(field, title=title) # prepare stream list if (not self.rowmap): for row in self.datamap: - for x in xrange(2, len(row)): + for x in range(2, len(row)): self.rowmap.append(row[x][0]) # load default category if (self.current): self.load(self.current) @@ -181,11 +181,11 @@ # load data, # update treeview content def load(self, category, force=False): # get data from cache or download - if (force or not self.streams.has_key(category)): + if (force or not category in self.streams): new_streams = self.update_streams(category) if new_streams: # modify @@ -235,11 +235,11 @@ # finds differences in new/old streamlist, marks deleted with flag def deleted_streams(self, new, old): diff = [] new = [row.get("url","http://example.com/") for row in new] for row in old: - if (row.has_key("url") and (row.get("url") not in new)): + if (url in row and (row.get("url") not in new)): row["deleted"] = 1 diff.append(row) return diff @@ -249,11 +249,11 @@ # oh my, at least it's working # at start the bookmarks module isn't fully registered at instantiation in parent.channels{} - might want to do that step by step rather # then display() is called too early to take effect - load() & co should actually be postponed to when a notebook tab gets selected first # => might be fixed now, 1.9.8 # state icon: bookmark star - if (conf.show_bookmarks and self.parent.channels.has_key("bookmarks") and self.parent.bookmarks.is_in(streams[i].get("url", "file:///tmp/none"))): + if (conf.show_bookmarks and "bookmarks" in self.parent.channels and self.parent.bookmarks.is_in(streams[i].get("url", "file:///tmp/none"))): streams[i]["favourite"] = 1 # state icon: INFO or DELETE if (not row.get("state")): if row.get("favourite"): @@ -309,15 +309,14 @@ # if category tree is empty, initialize it if not self.categories: __print__(dbg.PROC, "first_show: reload_categories"); #self.parent.thread(self.reload_categories) - print("reload categories"); self.reload_categories() self.display_categories() self.current = self.categories.keys()[0] - print self.current + __print__(dbg.STAT, self.current) self.load(self.current) # load current category else: __print__(dbg.STAT, "first_show: load current category"); DELETED channels/basicch.py Index: channels/basicch.py ================================================================== --- channels/basicch.py +++ channels/basicch.py @@ -1,305 +0,0 @@ - -# api: streamtuner2 -# title: basic.ch channel -# -# -# -# Porting ST1 plugin senseless, old parsing method wasn't working any longer. Static -# realaudio archive is not available anymore. -# -# Needs manual initialisation of categories first. -# - - -import re -import http -from config import conf -from channels import * -from xml.sax.saxutils import unescape - - - - - - - - - - - - - - -# basic.ch broadcast archive -class basicch (ChannelPlugin): - - # description - title = "basic.ch" - module = "basicch" - homepage = "http://www.basic.ch/" - version = 0.3 - base = "http://basic.ch/" - - # keeps category titles->urls - catmap = {} - categories = [] #"METAMIX", "reload category tree!", ["menu > channel > reload cat..."]] - #titles = dict(listeners=False, bitrate=False) - - - # read previous channel/stream data, if there is any - def cache(self): - ChannelPlugin.cache(self) - # catmap - cache = conf.load("cache/catmap_" + self.module) - if (cache): - self.catmap = cache - pass - - - # refresh category list - def update_categories(self): - - html = http.get(self.base + "shows.cfm") - - rx_current = re.compile(r""" - ([^<>]+) - \s*([^<>]+)
]+)">(METAMIX[^<]+) - \s+([^<>]+)
(http://[^<">]+)\s* - ([^<>]+)\s* - ([^<>]+) - """, re.S|re.X) - - entries = [] - - #-- update categories first - if not len(self.catmap): - self.update_categories() - - #-- frontpage mixes - if cat == "METAMIX": - for uu in rx_metamix.findall(http.get(self.base)): - (url, title, genre) = uu - entries.append({ - "genre": genre, - "title": title, - "url": url, - "format": "audio/mp3", - "homepage": self.homepage, - }) - - #-- pseudo entry - elif cat=="shows": - entries = [{"title":"shows","homepage":self.homepage+"shows.cfm"}] - - #-- fetch playlist.xml - else: - - # category name "Xsound & Ymusic" becomes "Xsoundandymusic" - id = cat.replace("&", "and").replace(" ", "") - id = id.lower().capitalize() - - catinfo = self.catmap.get(cat, {"id":"", "genre":""}) - - # extract - html = http.get(self.base + "playlist/" + id + ".xml") - for uu in rx_playlist.findall(html): # you know, we could parse this as proper xml - (url, artist, title) = uu # but hey, lazyness works too - entries.append({ - "url": url, - "title": artist, - "playing": title, - "genre": catinfo["genre"], - "format": "audio/mp3", - "homepage": self.base + "shows.cfm?showid=" + catinfo["id"], - }) - - # done - return entries - - - - - - - - - - - -# basic.ch broadcast archive -class basicch_old_static: #(ChannelPlugin): - - # description - title = "basic.ch" - module = "basicch" - homepage = "http://www.basic.ch/" - version = 0.2 - base = "http://basic.ch/" - - # keeps category titles->urls - catmap = {} - - - # read previous channel/stream data, if there is any - def cache(self): - ChannelPlugin.cache(self) - # catmap - cache = conf.load("cache/catmap_" + self.module) - if (cache): - self.catmap = cache - pass - - - # refresh category list - def update_categories(self): - - html = http.get(self.base + "downtest.cfm") - - rx_current = re.compile(""" - href="javascript:openWindow.'([\w.?=\d]+)'[^>]+> - (\w+[^<>]+)(\w+[^<>]+)]+)" - """, re.S|re.X) - - rx_archive = re.compile(""" - href="javascript:openWindow.'([\w.?=\d]+)'[^>]+>.+? - color="000000">(\w+[^<>]+)(\w+[^<>]+)(\d\d\.\d\d\.\d\d).+? - href="(http://[^">]+|/ram/\w+.ram)"[^>]*>([^<>]+) - .+? (>(\w+[^<]*)Radio Genres - rx = re.compile(r'[\w\s]+', re.S) + rx = re.compile(r'[\w\s]+', re.S) sub = [] for uu in rx.findall(html): __print__( dbg.DATA, uu ) - (main,name,id) = uu + (main,name,id) = uu name = urllib.unquote(name) # main category if main: if sub: Index: channels/timer.py ================================================================== --- channels/timer.py +++ channels/timer.py @@ -18,10 +18,11 @@ # are stored in the description field, and can thus be edited. However, after editing # times manually, streamtuner2 must be restarted for the changes to take effect. # +from config import __print__, dbg from channels import * import kronos from mygtk import mygtk from action import action import copy @@ -74,11 +75,11 @@ # prepare spool self.sched = kronos.ThreadedScheduler() for row in self.streams: try: self.queue(row) - except Exception,e: print("queuing error", e) + except Exception as e: __print__(dbg.ERR, "queuing error", e) self.sched.start() # display GUI for setting timespec def edit_timer(self, *w): DELETED channels/tv.py Index: channels/tv.py ================================================================== --- channels/tv.py +++ channels/tv.py @@ -1,123 +0,0 @@ -# -# api: streamtuner2 -# title: shoutcast TV -# description: TV listings from shoutcast -# version: 0.0 -# stolen-from: Tunapie.sf.net -# -# As seen in Tunapie, there are still TV listings on Shoutcast. This module -# adds a separate tab for them. Streamtuner2 is centrally and foremost about -# radio listings, so this plugin will remain one of the few exceptions. -# -# http://www.shoutcast.com/sbin/newtvlister.phtml?alltv=1 -# -# Pasing with lxml is dead simple in this case, so we use etree directly -# instead of PyQuery. Like with the Xiph plugin, downloaded streams are simply -# stored in .streams["all"] pseudo-category. -# -# icon: http://cemagraphics.deviantart.com/art/Little-Tv-Icon-96461135 - -from channels import * -import http -import lxml.etree - - - - -# TV listings from shoutcast.com -class tv(ChannelPlugin): - - # desc - api = "streamtuner2" - module = "tv" - title = "TV" - version = 0.1 - homepage = "http://www.shoutcast.com/" - base_url = "http://www.shoutcast.com/sbin/newtvlister.phtml?alltv=1" - play_url = "http://yp.shoutcast.com/sbin/tunein-station.pls?id=" - listformat = "audio/x-scpls" # video streams are NSV linked in PLS format - - # settings - config = [ - ] - - # categories - categories = ["all", "video"] - current = "" - default = "all" - empty = "" - - # redefine - streams = {} - - - # get complete list - def all(self): - r = [] - - # retrieve - xml = http.get(self.base_url) - - # split up entries - for station in lxml.etree.fromstring(xml): - - r.append({ - "title": station.get("name"), - "playing": station.get("ct"), - "id": station.get("id"), - "url": self.play_url + station.get("id"), - "format": "video/nsv", - "time": station.get("rt"), - "extra": station.get("load"), - "genre": station.get("genre"), - "bitrate": int(station.get("br")), - "listeners": int(station.get("lc")), - }) - - return r - - - # genre switch - def load(self, cat, force=False): - if force or not self.streams.get("all"): - self.streams["all"] = self.all() - ChannelPlugin.load(self, cat, force) - - - # update from the list - def update_categories(self): - - # update it always here: #if not self.streams.get("all"): - self.streams["all"] = self.all() - - # enumerate categories - c = {"all":100000} - for row in self.streams["all"]: - for genre in row["genre"].split(" "): - if len(genre)>2 and row["bitrate"]>=200: - c[genre] = c.get(genre, 0) + 1 - # append - self.categories = sorted(c, key=c.get, reverse=True) - - - - # extract from big list - def update_streams(self, cat, search=""): - - # autoload only if "all" category is missing - if not self.streams.get("all"): - self.streams["all"] = self.all() - - # return complete list as-is - if cat == "all": - return self.streams[cat] - - # search for category - else: - return [row for row in self.streams["all"] if row["genre"].find(cat)>=0] - - - - - - ADDED channels/xiph.png Index: channels/xiph.png ================================================================== --- channels/xiph.png +++ channels/xiph.png cannot compute difference between binary files Index: channels/xiph.py ================================================================== --- channels/xiph.py +++ channels/xiph.py @@ -17,11 +17,11 @@ # streamtuner2 modules from config import conf from mygtk import mygtk -import http +import ahttp as http from channels import * from config import __print__, dbg # python modules import re Index: cli.py ================================================================== --- cli.py +++ cli.py @@ -15,11 +15,11 @@ # import sys #from channels import * -import http +import ahttp import action from config import conf import json ADDED compat2and3.py Index: compat2and3.py ================================================================== --- compat2and3.py +++ compat2and3.py @@ -0,0 +1,60 @@ +# +# encoding: UTF-8 +# api: python +# type: functions +# title: Python2 and Python3 compatibility +# version: 0.1 +# +# Renames some Python3 modules into their Py2 equivalent. +# Slim local alternative to `six` module. +# + + +import sys + + +# Python 2 +if sys.version_info < (3,0): + + # version tags + PY2 = 1 + PY3 = 0 + + # basic functions + xrange = xrange + range = xrange + unicode = unicode + + # urllib modules + import urllib + import urllib2 + from urllib import urlencode + import urlparse + import cookielib + + # filesys + from StringIO import StringIO + + +# Python 3 +else: + + # version tags + PY2 = 0 + PY3 = 1 + + # basic functions + xrange = range + unicode = str + + # urllib modules + import urllib.request as urllib + import urllib.request as urllib2 + from urllib.parse import urlencode + import urllib.parse as urlparse + from http import cookiejar as cookielib + + # filesys + from io import StringIO + + Index: config.py ================================================================== --- config.py +++ config.py @@ -15,22 +15,24 @@ # import os import sys -import pson +import json import gzip import platform + + #-- create a single instance of config object conf = object() - #-- global configuration data --------------------------------------------- class ConfigDict(dict): + # start def __init__(self): # object==dict means conf.var is conf["var"] @@ -50,10 +52,11 @@ self.update(last) # store defaults in file else: self.save("settings") self.firstrun = 1 + # some defaults def defaults(self): self.browser = "sensible-browser" self.play = { @@ -87,10 +90,11 @@ self.debug = False self.channel_order = "shoutcast, xiph, internet_radio_org_uk, jamendo, myoggradio, .." self.reuse_m3u = 1 self.google_homepage = 1 self.windows = platform.system()=="Windows" + self.debug = 1 # each plugin has a .config dict list, we add defaults here def add_plugin_defaults(self, config, module=""): @@ -119,11 +123,11 @@ # store some configuration list/dict into a file def save(self, name="settings", data=None, gz=0, nice=0): name = name + ".json" - if (data == None): + if (data is None): data = dict(self.__dict__) # ANOTHER WORKAROUND: typecast to plain dict(), else json filter_data sees it as object and str()s it nice = 1 # check for subdir if (name.find("/") > 0): subdir = name[0:name.find("/")] @@ -139,11 +143,11 @@ if os.path.exists(file): os.unlink(file) else: f = open(file, "w") # encode - pson.dump(data, f, indent=(4 if nice else None)) + json.dump(data, f, indent=(4 if nice else None)) f.close() # retrieve data from config file def load(self, name): @@ -150,21 +154,21 @@ name = name + ".json" file = self.dir + "/" + name try: # .gz or normal file if os.path.exists(file + ".gz"): - f = gzip.open(file + ".gz", "r") + f = gzip.open(file + ".gz", "rt") elif os.path.exists(file): - f = open(file, "r") + f = open(file, "rt") else: return # file not found # decode - r = pson.load(f) + r = json.load(f) f.close() return r except Exception as e: - print("PSON parsing error (in "+name+")", e) + print(dbg.ERR, "PSON parsing error (in "+name+")", e) # recursive dict update def update(self, with_new_data): for key,value in with_new_data.items(): @@ -179,15 +183,10 @@ def find_in_dirs(self, dirs, file): for d in dirs: if os.path.exists(d+"/"+file): return d+"/"+file - - -#-- actually fill global conf instance -conf = ConfigDict() - # wrapper for all print statements def __print__(*args): @@ -205,7 +204,16 @@ "HTTP": "[HTTP]", # magenta HTTP REQUEST "DATA": "[DATA]", # cyan DATA "INFO": "[INFO]", # gray INFO "STAT": "[STATE]", # gray CONFIG STATE }) + + + +#-- actually fill global conf instance +conf = ConfigDict() +if conf: + __print__(dbg.PROC, "ConfigDict() initialized") + + Index: favicon.py ================================================================== --- favicon.py +++ favicon.py @@ -26,17 +26,16 @@ delete_google_stub = 1 # don't keep placeholder images google_placeholder_filesizes = (726,896) import os, os.path -import urllib +from compat2and3 import xrange, urllib import re -import urlparse from config import conf try: from processing import Process as Thread except: from threading import Thread -import http +import ahttp # ensure that we don't try to download a single favicon twice per session, # if it's not available the first time, we won't get it after switching stations back and forth @@ -87,16 +86,16 @@ title = rx_t.search(row["title"]) if title: title = title.group(0).replace(" ", "%20") # do a google search - html = http.ajax("http://www.google.de/search?hl=de&q="+title, None) + html = ahttp.ajax("http://www.google.de/search?hl=de&q="+title, None) # find first URL hit url = rx_u.search(html) if url: - row["homepage"] = http.fix_url(url.group(1)) + row["homepage"] = ahttp.fix_url(url.group(1)) pass #----------------- @@ -193,13 +192,13 @@ r = urllib.urlopen(favicon) headers = r.info() # abort on if r.getcode() >= 300: - raise "HTTP error", r.getcode() + raise Error("HTTP error" + r.getcode()) if not headers["Content-Type"].lower().find("image/"): - raise "can't use text/* content" + raise Error("can't use text/* content") # save file fn_tmp = fn+".tmp" f = open(fn_tmp, "wb") f.write(r.read(32768)) @@ -234,11 +233,11 @@ # url or if favicon.startswith("http://"): None # just /pathname else: - favicon = urlparse.urljoin(url, favicon) + favicon = ahttp.urlparse.urljoin(url, favicon) #favicon = "http://" + domain(url) + "/" + favicon # download direct_download(favicon, file(url)) @@ -264,11 +263,11 @@ import operator import struct try: from PIL import BmpImagePlugin, PngImagePlugin, Image -except Exception, e: +except Exception as e: print("no PIL", e) always_google = 1 only_google = 1 Index: gtk3.xml ================================================================== --- gtk3.xml +++ gtk3.xml @@ -1,17 +1,18 @@ + - + False + 0.95999999999999996 5 station search center-on-parent dialog False center - 0.95999999999999996 True @@ -27,11 +28,10 @@ cancel False True True True - False False False @@ -41,11 +41,10 @@ False True True - False False False 1 @@ -57,11 +56,10 @@ False True True True Instead of searching in the station list, just look up the above search term on google. - False half False @@ -75,11 +73,10 @@ False True False True Instead of doing a cache search, go through the search functions on the directory service homepages. (UNIMPLEMENTED) - False half False @@ -95,14 +92,13 @@ False True True True Start searching for above search term in the currently loaded station lists. Doesn't find *new* information, just looks through the known data. - False True - + False False 4 @@ -277,18 +273,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all channels False True True False - False 0.5 True True @@ -363,11 +493,10 @@ False True True True - False none False False @@ -379,11 +508,10 @@ in title False True True False - False 0.5 True True @@ -397,11 +525,10 @@ in description False True True False - False 0.5 True True @@ -415,11 +542,10 @@ any fields False True True False - False 0.5 True True @@ -432,11 +558,10 @@ False True True True - False none False False @@ -462,11 +587,10 @@ False True True True - False none False False @@ -478,11 +602,10 @@ homepage url False True True False - False 0.5 True True @@ -496,11 +619,10 @@ extra info False True True False - False 0.5 True True @@ -513,11 +635,10 @@ and genre False True True False - False 0.5 True True @@ -530,11 +651,10 @@ False True True True - False none False False @@ -690,11 +810,10 @@ cancel False True True True - False False False @@ -706,11 +825,10 @@ ok False True True True - False False False @@ -783,10 +901,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True True @@ -799,10 +971,19 @@ 2 1 2 + + + + + + + + + @@ -1010,11 +1191,10 @@ 200 20 True True - True False False 1 @@ -1028,11 +1208,10 @@ 200 20 True True - True False False 1 @@ -1046,11 +1225,10 @@ 200 20 True True - True False False 1 @@ -1064,11 +1242,10 @@ 200 20 True True - True False False 1 @@ -1112,11 +1289,10 @@ 200 20 True True - True False False 1 @@ -1168,11 +1344,10 @@ 200 20 True True - True False False 1 @@ -1186,11 +1361,10 @@ 200 20 True True - True False False 1 @@ -1215,11 +1389,10 @@ 200 20 True True - True False False 1 @@ -1367,18 +1540,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + show bookmark star for favourites in stream lists False True True False - False 0 True 1 @@ -1398,11 +1606,10 @@ 5 4 120 out - True False False True @@ -1434,11 +1641,10 @@ retain deleted stations in list False True True False - False 0 True 1 @@ -1452,11 +1658,10 @@ display favicons for individual music stations False True True False - False 0 True 1 @@ -1470,11 +1675,10 @@ load favicon for played stations False True True False - False 0 True 1 @@ -1488,11 +1692,10 @@ update favorites from freshened stream urls False True True False - False 0 True 1 @@ -1506,11 +1709,10 @@ google for homepage URL if missing False True True False - False 0 True 1 @@ -1539,11 +1741,10 @@ True True - True False False True @@ -1564,11 +1765,10 @@ automatically save window state False True True False - False 0 True 1 @@ -1803,10 +2003,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True False Directories @@ -1833,11 +2105,10 @@ 200 20 True True - True False False 1 @@ -1908,11 +2179,10 @@ 20 True True False - True False False 1 @@ -1926,11 +2196,10 @@ reuse temporary .m3u files False True True False - False 0.5 True 1 @@ -2244,11 +2513,10 @@ 100 35 True True True - False False True @@ -2262,11 +2530,10 @@ 100 35 True True True - False True True @@ -2293,16 +2560,16 @@ False + 0.94999999999999996 5 inspect/edit stream data center-on-parent True False - 0.94999999999999996 True False @@ -2492,11 +2759,10 @@ 100 25 True True True - False 100 10 @@ -2510,11 +2776,10 @@ 25 True True True Save changes. - False 210 10 @@ -2527,11 +2792,10 @@ 50 25 True True True - False 5 10 @@ -2631,15 +2895,10 @@ streamtuner2 980 775 /usr/share/pixmaps/streamtuner2.png applications-multimedia - - - streamtuner2 - - True False @@ -2674,13 +2933,13 @@ False True False bookmark True - + - + gtk-save-as @@ -2687,13 +2946,13 @@ False True False True True - + - + gtk-edit @@ -2700,14 +2959,14 @@ False True False True True - - + - + + False @@ -2741,12 +3000,12 @@ False True False True True - + @@ -2780,13 +3039,13 @@ False True False True True - + - + gtk-find @@ -2793,12 +3052,12 @@ False True False True True - + True @@ -2908,12 +3167,12 @@ False True False True True - + @@ -2944,12 +3203,12 @@ False True False Reload True - + False @@ -3058,11 +3317,10 @@ False True False - False play gtk-media-play @@ -3073,11 +3331,10 @@ False True False - False record gtk-media-record @@ -3088,11 +3345,10 @@ False True False - False station gtk-home @@ -3113,11 +3369,10 @@ False True False - False reload gtk-refresh @@ -3128,11 +3383,10 @@ False True False - False stop gtk-cancel @@ -3356,8 +3610,13 @@ end 2 + + + + streamtuner2 + Index: help/streamtuner2.1 ================================================================== --- help/streamtuner2.1 +++ help/streamtuner2.1 @@ -1,6 +1,5 @@ -.\" this is one of the nanoweb man pages .\" (many thanks to the manpage howto!) .\" .TH streamtuner2 "January 2014" "BSD/Linux" "User Manuals" .SH NAME streamtuner2 \- Browser for internet radio stations @@ -58,14 +57,23 @@ returns all info about the first match as JSON output. .TP .BI streamtuner2 " play frequence3" Looks for the first occourence, and starts the audio player for FREQUENCE3. + +.SH GRAPHICAL MODE + +There's only one option for the graphical UI mode: +.TP +.BI --gtk3 +Loads Gtk3 via PyGI instead of Gtk2. This is implicit when running on Python3 +anyway. + .SH FILES .IR /home/ $USER /.config/streamtuner2/settings.json .SH "SEE ALSO" .BR streamripper (1) .BR audacious (1) .BR json (5) .BR m3u (5) .BR pls (5) DELETED http.py Index: http.py ================================================================== --- http.py +++ http.py @@ -1,224 +0,0 @@ -# -# encoding: UTF-8 -# api: streamtuner2 -# type: functions -# title: http download / methods -# description: http utility -# version: 1.3 -# -# Provides a http GET method with gtk.statusbar() callback. -# And a function to add trailings slashes on http URLs. -# -# The latter code is pretty much unreadable. But let's put the -# blame on urllib2, the most braindamaged code in the Python -# standard library. -# - -try: - import urllib2 - from urllib import urlencode -except: - import urllib.request as urllib2 - import urllib.parse.urlencode as urlencode -import config -from config import __print__, dbg - - - -#-- url download --------------------------------------------- - - - -#-- chains to progress meter and status bar in main window -feedback = None - -# sets either text or percentage, so may take two parameters -def progress_feedback(*args): - - # use reset values if none given - if not args: - args = ["", 1.0] - - # send to main win - if feedback: - try: [feedback(d) for d in args] - except: pass - - - - -#-- GET -def get(url, maxsize=1<<19, feedback="old"): - __print__("GET", url) - - # statusbar info - progress_feedback(url, 0.0) - - # read - content = "" - f = urllib2.urlopen(url) - max = 222000 # mostly it's 200K, but we don't get any real information - read_size = 1 - - # multiple steps - while (read_size and len(content) < maxsize): - - # partial read - add = f.read(8192) - content = content + add - read_size = len(add) - - # set progress meter - progress_feedback(float(len(content)) / float(max)) - - # done - - # clean statusbar - progress_feedback() - - # fin - __print__(len(content)) - return content - - - - - -#-- fix invalid URLs -def fix_url(url): - if url is None: - url = "" - if len(url): - # remove whitespace - url = url.strip() - # add scheme - if (url.find("://") < 0): - url = "http://" + url - # add mandatory path - if (url.find("/", 10) < 0): - url = url + "/" - return url - - - - -# default HTTP headers for AJAX/POST request -default_headers = { - "User-Agent": "streamtuner2/2.1 (X11; U; Linux AMD64; en; rv:1.5.0.1) like WinAmp/2.1 but not like Googlebot/2.1", #"Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6", - "Accept": "*/*;q=0.5, audio/*, url/*", - "Accept-Language": "en-US,en,de,es,fr,it,*;q=0.1", - "Accept-Encoding": "gzip,deflate", - "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.1", - "Keep-Alive": "115", - "Connection": "keep-alive", - #"Content-Length", "56", - #"Cookie": "s_pers=%20s_getnr%3D1278607170446-Repeat%7C1341679170446%3B%20s_nrgvo%3DRepeat%7C1341679170447%3B; s_sess=%20s_cc%3Dtrue%3B%20s_sq%3Daolshtcst%252Caolsvc%253D%252526pid%25253Dsht%25252520%2525253A%25252520SHOUTcast%25252520Radio%25252520%2525257C%25252520Search%25252520Results%252526pidt%25253D1%252526oid%25253Dfunctiononclick%25252528event%25252529%2525257BshowMoreGenre%25252528%25252529%2525253B%2525257D%252526oidt%25253D2%252526ot%25253DDIV%3B; aolDemoChecked=1.849061", - "Pragma": "no-cache", - "Cache-Control": "no-cache", -} - - - -# simulate ajax calls -def ajax(url, post, referer=""): - - # request - headers = default_headers - headers.update({ - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", - "X-Requested-With": "XMLHttpRequest", - "Referer": (referer if referer else url), - }) - if type(post) == dict: - post = urlencode(post) - request = urllib2.Request(url, post, headers) - - # open url - __print__( vars(request) ) - progress_feedback(url, 0.2) - r = urllib2.urlopen(request) - - # get data - __print__( r.info() ) - progress_feedback(0.5) - data = r.read() - progress_feedback() - return data - - - -# http://techknack.net/python-urllib2-handlers/ -from gzip import GzipFile -from StringIO import StringIO -class ContentEncodingProcessor(urllib2.BaseHandler): - """A handler to add gzip capabilities to urllib2 requests """ - - # add headers to requests - def http_request(self, req): - req.add_header("Accept-Encoding", "gzip, deflate") - return req - - # decode - def http_response(self, req, resp): - old_resp = resp - # gzip - if resp.headers.get("content-encoding") == "gzip": - gz = GzipFile( - fileobj=StringIO(resp.read()), - mode="r" - ) - resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code) - resp.msg = old_resp.msg - # deflate - if resp.headers.get("content-encoding") == "deflate": - gz = StringIO( deflate(resp.read()) ) - resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code) # 'class to add info() and geturl() methods to an open file.' - resp.msg = old_resp.msg - return resp - -# deflate support -import zlib -def deflate(data): # zlib only provides the zlib compress format, not the deflate format; - try: # so on top of all there's this workaround: - return zlib.decompress(data, -zlib.MAX_WBITS) - except zlib.error: - return zlib.decompress(data) - - - - - - - -#-- init for later use -if urllib2: - - # config 1 - handlers = [None, None, None] - - # base - handlers[0] = urllib2.HTTPHandler() - if config.conf.debug: - handlers[0].set_http_debuglevel(3) - - # content-encoding - handlers[1] = ContentEncodingProcessor() - - # store cookies at runtime - import cookielib - cj = cookielib.CookieJar() - handlers[2] = urllib2.HTTPCookieProcessor( cj ) - - # inject into urllib2 - urllib2.install_opener( urllib2.build_opener(*handlers) ) - - - - -# alternative function names -AJAX=ajax -POST=ajax -GET=get -URL=fix_url - - Index: kronos.py ================================================================== --- kronos.py +++ kronos.py @@ -272,11 +272,11 @@ def _run(self): # Low-level run method to do the actual scheduling loop. while self.running: try: self.sched.run() - except Exception,x: + except Exception as x: print >>sys.stderr, "ERROR DURING SCHEDULER EXECUTION",x print >>sys.stderr, "".join( traceback.format_exception(*sys.exc_info())) print >>sys.stderr, "-" * 20 # queue is empty; sleep a short while before checking again @@ -296,11 +296,11 @@ def __call__(self, schedulerref): """Execute the task action in the scheduler's thread.""" try: self.execute() - except Exception,x: + except Exception as x: self.handle_exception(x) self.reschedule(schedulerref()) def reschedule(self, scheduler): """This method should be defined in one of the sub classes!""" @@ -464,11 +464,11 @@ def threadedcall(self): # This method is run within its own thread, so we have to # do the execute() call and exception handling here. try: self.execute() - except Exception,x: + except Exception as x: self.handle_exception(x) class ThreadedIntervalTask(ThreadedTaskMixin, IntervalTask): """Interval Task that executes in its own thread.""" pass @@ -531,11 +531,11 @@ pid = os.fork() if pid == 0: # we are the child try: self.execute() - except Exception,x: + except Exception as x: self.handle_exception(x) os._exit(0) else: # we are the parent self.reschedule(schedulerref()) Index: mygtk.py ================================================================== --- mygtk.py +++ mygtk.py @@ -2,11 +2,11 @@ # encoding: UTF-8 # api: python # type: functions # title: mygtk helper functions # description: simplify usage of some gtk widgets -# version: 1.6 +# version: 1.7 # author: mario # license: public domain # # # Wrappers around gtk methods. The TreeView method .columns() allows @@ -27,38 +27,42 @@ # debug from config import __print__, dbg +# filesystem +import os.path +import copy +import sys + +from compat2and3 import unicode, xrange, PY3 + -# gtk modules -gtk = 0 # 0=gtk2, else gtk3 -if gtk: +# gtk version (2=gtk2, 3=gtk3) +ver = 2 +# if running on Python3 or with commandline flag +if PY3 or "--gtk3" in sys.argv: + ver = 3 +# load gtk modules +if ver==3: from gi import pygtkcompat as pygtk pygtk.enable() pygtk.enable_gtk(version='3.0') from gi.repository import Gtk as gtk from gi.repository import GObject as gobject from gi.repository import GdkPixbuf ui_file = "gtk3.xml" - __print__(gtk) - __print__(gobject) -if not gtk: + empty_pixbuf = GdkPixbuf.Pixbuf.new_from_data(b"\0\0\0\0", GdkPixbuf.Colorspace.RGB, True, 8, 1, 1, 4, None, None) + __print__(dbg.PROC, gtk) + __print__(dbg.PROC, gobject) +else: import pygtk import gtk import gobject ui_file = "gtk2.xml" - -# filesystem -import os.path -import copy - - -try: - empty_pixbuf = gtk.gdk.pixbuf_new_from_data(b"\0\0\0\0",gtk.gdk.COLORSPACE_RGB,True,8,1,1,4) -except: - empty_pixbuf = GdkPixbuf.Pixbuf.new_from_data(b"\0\0\0\0", GdkPixbuf.Colorspace.RGB, True, 8, 1, 1, 4, None, None) + empty_pixbuf = gtk.gdk.pixbuf_new_from_data(b"\0\0\0\0",gtk.gdk.COLORSPACE_RGB,True,8,1,1,4) + # simplified gtk constructors --------------------------------------------- class mygtk: @@ -124,16 +128,16 @@ #col.set_sort_column_id(datapos) # only on textual cells # attach cell to column col.pack_end(rend, expand=cell[3].get("expand",True)) # apply attributes - for attr,val in cell[3].iteritems(): + for attr,val in list(cell[3].items()): col.add_attribute(rend, attr, val) # next datapos += 1 - __print__(cell) + __print__(dbg.INFO, cell, len(cell)) # add column to treeview widget.append_column(col) # finalize widget widget.set_search_column(5) #?? widget.set_search_column(4) #?? @@ -152,12 +156,12 @@ for var in xrange(2, len(desc)): vartypes.append(desc[var][1]) # content types rowmap.append(desc[var][0]) # dict{} column keys in entries[] list # create gtk array storage ls = gtk.ListStore(*vartypes) # could be a TreeStore, too - __print__(vartypes) - __print__(rowmap) + __print__(dbg.UI, vartypes, len(vartypes)) + __print__(dbg.DATA, rowmap, len(rowmap)) # prepare for missing values, and special variable types defaults = { str: "", unicode: "", @@ -168,15 +172,22 @@ if gtk.gdk.Pixbuf in vartypes: pix_entry = vartypes.index(gtk.gdk.Pixbuf) # sort data into gtk liststore array for row in entries: -# row["search_col"] = "white" + + # preset some values if absent + row.setdefault("deleted", False) + row.setdefault("search_col", "#ffffff") + row.setdefault("search_set", False) # generate ordered list from dictionary, using rowmap association row = [ row.get( skey , defaults[vartypes[i]] ) for i,skey in enumerate(rowmap) ] + # map Python2 unicode to str + row = [ str(value) if type(value) is unicode else value for value in row ] + # autotransform string -> gtk image object if (pix_entry and type(row[pix_entry]) == str): row[pix_entry] = ( gtk.gdk.pixbuf_new_from_file(row[pix_entry]) if os.path.exists(row[pix_entry]) else defaults[gtk.gdk.Pixbuf] ) try: @@ -184,11 +195,11 @@ ls.append(row) # had to be adapted for real TreeStore (would require additional input for grouping/level/parents) except: # brute-force typecast ls.append( [va if ty==gtk.gdk.Pixbuf else ty(va) for va,ty in zip(row,vartypes)] ) - __print__(row) + __print__("→", row, len(row)) # apply array to widget widget.set_model(ls) return ls @@ -205,18 +216,19 @@ @staticmethod def tree(widget, entries, title="category", icon=gtk.STOCK_DIRECTORY): # list types ls = gtk.TreeStore(str, str) + print(entries) # add entries for entry in entries: - if (type(entry) == str): - main = ls.append(None, [entry, icon]) + if isinstance(entry, (str,unicode)): + main = ls.append(None, [str(entry), icon]) else: for sub_title in entry: - ls.append(main, [sub_title, icon]) + ls.append(main, [str(sub_title), icon]) # just one column tvcolumn = gtk.TreeViewColumn(title); widget.append_column(tvcolumn) @@ -300,11 +312,11 @@ for wn in r.keys(): # widgetnames w = wTree.get_widget(wn) if (not w): continue t = type(w) - for method,args in r[wn].iteritems(): + for method,args in r[wn].items(): # gtk.Window if method == "size": w.resize(args[0], args[1]) # gtk.TreeView if method == "columns:width": Index: pq.py ================================================================== --- pq.py +++ pq.py @@ -17,11 +17,11 @@ from pyquery import PyQuery as pq # pq.each_pq = lambda self,func: self.each( lambda i,html: func( pq(html, parser="html") ) ) -except Exception, e: +except Exception as e: # disable use pq = None config.conf.pyquery = False DELETED pson.py Index: pson.py ================================================================== --- pson.py +++ pson.py @@ -1,100 +0,0 @@ -# -# encoding: UTF-8 -# api: python -# type: functions -# title: json emulation -# description: simplify usage of some gtk widgets -# version: 1.7 -# author: mario -# license: public domain -# -# -# This module provides the JSON api. If the python 2.6 module -# isn't available, it provides an emulation using str() and -# eval() and Python notation. (The representations are close.) -# -# Additionally it filters out any left-over objects. Sometimes -# pygtk-objects crawled into the streams[] lists, because rows -# might have been queried from the widgets. -# - - -#-- reading and writing json (for the config module) ---------------------------------- - -import sys -if sys.version_info > (2, 9): - unicode = str - #dict.iteritems = dict.items - -# try to load the system module first -try: - from json import dump as json_dump, load as json_load -except: - print("no native Python JSON module") - - -#except: - # pseudo-JSON implementation - # - the basic python data types dict,list,str,int are mostly identical to json - # - therefore a basic str() conversion is enough for writing - # - for reading the more bothersome eval() is used - # - it's however no severe security problem here, because we're just reading - # local config files (written by us) and accept no data from outside / web - # NOTE: This code is only used, if the Python json module (since 2.6) isn't there. - - -# store object in string representation into filepointer -def dump(obj, fp, indent=0): - - obj = filter_data(obj) - - try: - return json_dump(obj, fp, indent=indent, sort_keys=(indent and indent>0)) - except: - return fp.write(str(obj)) - # .replace("'}, ", "'},\n ") # add whitespace - # .replace("', ", "',\n ")) - # .replace("': [{'", "':\n[\n{'") - pass - - -# load from filepointer, decode string into dicts/list -def load(fp): - try: - #print("try json") - r = json_load(fp) - r = filter_data(r) # turn unicode() strings back into str() - pygtk does not accept u"strings" - except: - #print("fall back on pson") - fp.seek(0) - r = eval(fp.read(1<<27)) # max 128 MB - # print("fake json module: in python variable dump notation") - - if r == None: - r = {} - return r - - -# removes any objects, turns unicode back into str -def filter_data(obj): - if type(obj) in (int, float, bool, str): - return obj -# elif type(obj) == str: #->str->utf8->str -# return str(unicode(obj)) - elif type(obj) == unicode: - return str(obj) - elif type(obj) in (list, tuple, set): - obj = list(obj) - for i,v in enumerate(obj): - obj[i] = filter_data(v) - elif type(obj) == dict: - for i,v in list(obj.items()): - i = filter_data(i) - obj[i] = filter_data(v) - else: - print("invalid object in data, converting to string: ", type(obj), obj) - obj = str(obj) - return obj - - - Index: st2.py ================================================================== --- st2.py +++ st2.py @@ -3,11 +3,11 @@ # api: python # type: application # title: streamtuner2 # description: directory browser for internet radio / audio streams # depends: gtk, pygtk, xml.dom.minidom, threading, lxml, pyquery, kronos -# version: 2.0.9.5 +# version: 2.0.9.7 # author: mario salzer # license: public domain # url: http://freshmeat.net/projects/streamtuner2 # config: # category: multimedia @@ -30,24 +30,17 @@ """ project status """ # # The application runs mostly stable. The GUI interfaces are workable. +# It's supposed to run on Gtk2 and Gtk3. Python3 support is still WIP. # There haven't been any optimizations regarding memory usage and -# performance. The current internal API is acceptable. Documentation is -# coming up. +# performance. The current internal API is vastly undocumented. # # current bugs: # - audio- and list-format support is not very robust / needs better API -# - lots of GtkWarning messages # - not all keyboard shortcuts work -# - in-list search doesn't work in our treeviews (???) -# - JSON files are only trouble: loading of data files might lead to more -# errors now, even if pson module still falls back on old method -# (unicode strings from json.load are useless to us, require typecasts) -# (nonsupport of tuples led to regression in mygtk.app_restore) -# (sometimes we receive 8bit-content, which the json module can't save) # # features: # - treeview lists are created from datamap[] structure and stream{} dicts # - channel categories are built-in defaults (can be freshened up however) # - config vars and cache data get stored as JSON in ~/.config/streamtuner2/ @@ -66,24 +59,17 @@ # as they are passed to gtk.gdk.Pixbuf (OTOH data pre-filtered by Google) # - MEDIUM: audio players / decoders are easily affected by buffer overflows # from corrupt mp3/stream data, and streamtuner2 executes them # - but since that's the purpose -> no workaround # -# still help wanted on: -# - any of the above -# - new plugins (local file viewer) -# - nicer logo (or donations accepted to consult graphics designer) -# # standard modules import sys import os, os.path import re -import copy -import urllib # threading or processing module try: from processing import Process as Thread except: @@ -90,23 +76,22 @@ from threading import Thread Thread.stop = lambda self: None # add library path sys.path.insert(0, "/usr/share/streamtuner2") # pre-defined directory for modules -sys.path.insert(0, ".") # pre-defined directory for modules +sys.path.insert(0, ".") # development module path # gtk modules -from mygtk import pygtk, gtk, gobject, ui_file, mygtk +from mygtk import pygtk, gtk, gobject, ui_file, mygtk, ver as GTK_VER # custom modules from config import conf # initializes itself, so all conf.vars are available right away from config import __print__, dbg -import http +import ahttp import action # needs workaround... (action.main=main) from channels import * import favicon -#from pq import pq # this represents the main window # and also contains most application behaviour @@ -168,11 +153,11 @@ except: __print__(dbg.INIT, "main.__init__: current_channel.first_show() initialization error") # bind gtk/glade event names to functions gui_startup(19/20.0) - self.connect_signals(dict( { + self.connect_signals(dict( list({ "gtk_main_quit" : self.gtk_main_quit, # close window # treeviews / notebook "on_stream_row_activated" : self.on_play_clicked, # double click in a streams list "on_category_clicked": self.on_category_clicked, # new selection in category list "on_notebook_channels_switch_page": self.channel_switch, # channel notebook tab changed @@ -218,11 +203,11 @@ "true": lambda w,*args: True, "streamedit_open": streamedit.open, "streamedit_save": streamedit.save, "streamedit_new": streamedit.new, "streamedit_cancel": streamedit.cancel, - }.items() + self.add_signals.items() )) + }.items() ) + list( self.add_signals.items() ) )) # actually display main window gui_startup(99/100.0) self.win_streamtuner2.show() @@ -235,19 +220,19 @@ #-- Shortcut for glade.get_widget() # Allows access to widgets as direct attributes instead of using .get_widget() # Also looks in self.channels[] for the named channel plugins def __getattr__(self, name): - if (self.channels.has_key(name)): + if (name in self.channels): return self.channels[name] # like self.shoutcast else: return self.get_object(name) # or gives an error if neither exists # custom-named widgets are available from .widgets{} not via .get_widget() def get_widget(self, name): - if self.widgets.has_key(name): + if name in self.widgets: return self.widgets[name] else: return gtk.Builder.get_object(self, name) @@ -523,11 +508,14 @@ # end application and gtk+ main loop def gtk_main_quit(self, widget, *x): if conf.auto_save_appstate: - self.app_state(widget) + try: # doesn't work with gtk3 yet (probably just hooking at the wrong time) + self.app_state(widget) + except: + None gtk.main_quit() @@ -558,11 +546,15 @@ # right-click ? if event.button >= 3: path = treeview.get_path_at_pos(int(event.x), int(event.y))[0] treeview.grab_focus() treeview.set_cursor(path, None, False) - main.streamactions.popup(None, None, None, event.button, event.time) + main.streamactions.popup( + parent_menu_shell=None, parent_menu_item=None, func=None, + button=event.button, activate_time=event.time, + data=None + ) return None # we need to pass on to normal left-button signal handler else: return False # this works better as callback function than as class - because of False/Object result for event trigger @@ -571,13 +563,13 @@ # encapsulates references to gtk objects AND properties in main window class auxiliary_window(object): def __getattr__(self, name): - if main.__dict__.has_key(name): + if name in main.__dict__: return main.__dict__[name] - elif StreamTunerTwo.__dict__.has_key(name): + elif name in StreamTunerTwo.__dict__: return StreamTunerTwo.__dict__[name] else: return main.get_widget(name) """ allows to use self. and main. almost interchangably """ @@ -770,11 +762,11 @@ return True # set/load values between gtk window and conf. dict def apply(self, config, prefix="config_", save=0): - for key,val in config.iteritems(): + for key,val in config.items(): # map non-alphanumeric chars from config{} to underscores in according gtk widget names id = re.sub("[^\w]", "_", key) w = main.get_widget(prefix + id) __print__(dbg.CONF, "config", ("save" if save else "load"), prefix+id, w, val) # recurse into dictionaries, transform: conf.play.audio/mp3 => conf.play_audio_mp3 @@ -826,11 +818,11 @@ once = 0 def add_plugins(self): if self.once: return - for name,enabled in conf.plugins.iteritems(): + for name,enabled in conf.plugins.items(): # add plugin load entry if name: label = ("enable ⎗ %s channel" if self.channels.get(name) else "use ⎗ %s plugin") cb = gtk.ToggleButton(label=label % name) @@ -1045,12 +1037,12 @@ # This step is most likely redundant, but prevents accidently re-rewriting # stations that are in two channels (=duplicates with different PLS urls). check = {"http//": "[row]"} check = dict((row["url"],row) for row in fav) # walk through all channels/streams - for chname,channel in main.channels.iteritems(): - for cat,streams in channel.streams.iteritems(): + for chname,channel in main.channels.items(): + for cat,streams in channel.streams.items(): # keep the potentially changed rows if (chname == updated_channel) and (cat == updated_category): freshened_streams = streams @@ -1151,11 +1143,11 @@ #-- global configuration settings "conf = Config()" # already happened with "from config import conf" # graphical - if len(sys.argv) < 2: + if len(sys.argv) < 2 or "--gtk3" in sys.argv: # prepare for threading in Gtk+ callbacks gobject.threads_init() gui_startup(1/100.0) @@ -1164,11 +1156,11 @@ main = StreamTunerTwo() # module coupling action.main = main # action (play/record) module needs a reference to main window for gtk interaction and some URL/URI callbacks action = action.action # shorter name - http.feedback = main.status # http module gives status feedbacks too + ahttp.feedback = main.status # http module gives status feedbacks too # first invocation if (conf.get("firstrun")): config_dialog.open(None) del conf.firstrun