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+[^<>]+)
- """, re.S|re.X)
-
- archive = []
- previous = []
- current = []
-
- #-- current listings with latest broadcast and archive link
- for uu in rx_current.findall(html):
- self.catmap[uu[1]] = {
- "sub_url": self.base + uu[0],
- "title": uu[1],
- "genre": uu[2],
- "url": uu[3],
- }
- archive.append(uu[1])
-
- #-- old listings only have archive link
- for uu in rx_archive.findall(html):
- self.catmap[uu[1]] = {
- "sub_url": self.base + uu[0],
- "genre": uu[2],
- }
- previous.append(uu[1])
-
- #-- populate category treee
- self.categories = [
- "current",
- "archive", archive,
- "previous", previous,
- ]
-
- #-- inject streams
- self.streams["current"] = [
- {
- "title": e["title"],
- "url": e["url"],
- "genre": e["genre"],
- "format": "audio/mp3",
- "listeners": 0,
- "max": 100,
- "bitrate": 0,
- "homepage": e["sub_url"],
- }
- for title,e in self.catmap.iteritems() if e.get("url")
- ]
-
- #-- keep catmap as cache-file, it's essential for redisplaying
- self.save()
- return
-
- # saves .streams and .catmap
- def save(self):
- ChannelPlugin.save(self)
- conf.save("cache/catmap_" + self.module, self.catmap)
-
-
- # download links from dmoz listing
- def update_streams(self, cat, force=0):
-
- if not self.catmap:
- self.update_categories()
- elif cat=="current":
- self.update_categories()
- return self.streams["current"]
-
- if not self.catmap.get(cat):
- return []
-
- e = self.catmap[cat]
- html = http.get( e["sub_url"] )
-
- rx_archives = re.compile("""
- >(\d\d\.\d\d\.\d\d).+?
- href="(http://[^">]+|/ram/\w+.ram)"[^>]*>([^<>]+)
- .+? (>(\w+[^<]*))?
- """, re.X|re.S)
-
- entries = []
-
- for uu in rx_archives.findall(html):
- url = uu[1]
- ram = url.find("http://") < 0
- if ram:
- url = self.base + url[1:]
- entries.append({
- "title": uu[0],
- "url": url,
- "playing": uu[2],
- "genre": e["genre"],
- "format": ( "audio/x-pn-realaudio" if ram else "audio/"+uu[3].lower() ),
- "listeners": 0,
- "max": 1,
- "homepage": e["sub_url"],
- })
-
- return entries
-
-
-
ADDED channels/google.png
Index: channels/google.png
==================================================================
--- channels/google.png
+++ channels/google.png
cannot compute difference between binary files
Index: channels/google.py
==================================================================
--- channels/google.py
+++ channels/google.py
@@ -49,11 +49,11 @@
import re, os, gtk
from channels import *
from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities
-import http
+import ahttp as http
### constants #################################################################
ADDED channels/internet_radio_org_uk.png
Index: channels/internet_radio_org_uk.png
==================================================================
--- channels/internet_radio_org_uk.png
+++ channels/internet_radio_org_uk.png
cannot compute difference between binary files
Index: channels/internet_radio_org_uk.py
==================================================================
--- channels/internet_radio_org_uk.py
+++ channels/internet_radio_org_uk.py
@@ -13,11 +13,11 @@
from channels import *
import re
from config import conf, __print__, dbg
-import http
+import ahttp as http
from pq import pq
ADDED channels/jamendo.png
Index: channels/jamendo.png
==================================================================
--- channels/jamendo.png
+++ channels/jamendo.png
cannot compute difference between binary files
Index: channels/jamendo.py
==================================================================
--- channels/jamendo.py
+++ channels/jamendo.py
@@ -6,11 +6,11 @@
# Requires more rework of streamtuner2 list display to show album covers.
#
import re
-import http
+import ahttp as http
from config import conf
from channels import *
from xml.sax.saxutils import unescape
Index: channels/links.py
==================================================================
--- channels/links.py
+++ channels/links.py
@@ -56,23 +56,23 @@
bookmarks.streams[self.module] = []
bookmarks.add_category(self.module)
# collect links from channel plugins
- for name,channel in parent.channels.iteritems():
+ for name,channel in parent.channels.items():
try:
self.streams.append({
"favourite": 1,
"title": channel.title,
"homepage": channel.homepage,
})
except: pass
- for title,homepage in self.default.iteritems():
+ for title,homepage in self.default.items():
self.streams.append({
"title": title,
"homepage": homepage,
})
# add to bookmarks
bookmarks.streams[self.module] = self.streams
ADDED channels/live365.png
Index: channels/live365.png
==================================================================
--- channels/live365.png
+++ channels/live365.png
cannot compute difference between binary files
Index: channels/live365.py
==================================================================
--- channels/live365.py
+++ channels/live365.py
@@ -8,11 +8,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
ADDED channels/modarchive.png
Index: channels/modarchive.png
==================================================================
--- channels/modarchive.png
+++ channels/modarchive.png
cannot compute difference between binary files
Index: channels/modarchive.py
==================================================================
--- channels/modarchive.py
+++ channels/modarchive.py
@@ -10,11 +10,11 @@
# VLC in */* seems to work fine however.
#
import re
-import http
+import ahttp as http
from config import conf
from channels import *
from config import __print__, dbg
from xml.sax.saxutils import unescape
ADDED channels/musicgoal.png
Index: channels/musicgoal.png
==================================================================
--- channels/musicgoal.png
+++ channels/musicgoal.png
cannot compute difference between binary files
Index: channels/musicgoal.py
==================================================================
--- channels/musicgoal.py
+++ channels/musicgoal.py
@@ -14,11 +14,11 @@
# st2 modules
from config import conf
from mygtk import mygtk
-import http
+import ahttp as http
from channels import *
# python modules
import re
import json
ADDED channels/myoggradio.png
Index: channels/myoggradio.png
==================================================================
--- channels/myoggradio.png
+++ channels/myoggradio.png
cannot compute difference between binary files
Index: channels/myoggradio.py
==================================================================
--- channels/myoggradio.py
+++ channels/myoggradio.py
@@ -24,11 +24,11 @@
from config import conf
from action import action
import re
import json
-from StringIO import StringIO
+from compat2and3 import StringIO
import copy
# open source radio sharing stie
ADDED channels/punkcast.png
Index: channels/punkcast.png
==================================================================
--- channels/punkcast.png
+++ channels/punkcast.png
cannot compute difference between binary files
Index: channels/punkcast.py
==================================================================
--- channels/punkcast.py
+++ channels/punkcast.py
@@ -7,11 +7,11 @@
# ST1 looked prettier with random images within.
#
import re
-import http
+import ahttp as http
from config import conf
import action
from channels import *
from config import __print__, dbg
ADDED channels/shoutcast.png
Index: channels/shoutcast.png
==================================================================
--- channels/shoutcast.png
+++ channels/shoutcast.png
cannot compute difference between binary files
Index: channels/shoutcast.py
==================================================================
--- channels/shoutcast.py
+++ channels/shoutcast.py
@@ -16,17 +16,17 @@
#
#
#
-import http
-import urllib
+import ahttp as http
import re
from config import conf, __print__, dbg
from pq import pq
#from channels import * # works everywhere but in this plugin(???!)
import channels
+from compat2and3 import urllib
# SHOUTcast data module ----------------------------------------
class shoutcast(channels.ChannelPlugin):
@@ -64,15 +64,15 @@
html = http.get(self.base_url)
self.categories = []
__print__( dbg.DATA, html )
# 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": "[35m[HTTP][0m", # magenta HTTP REQUEST
"DATA": "[36m[DATA][0m", # cyan DATA
"INFO": "[37m[INFO][0m", # gray INFO
"STAT": "[37m[STATE][0m", # 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
@@ -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
-
+
-
+
@@ -2780,13 +3039,13 @@
False
True
False
True
True
-
+
-
+
@@ -2944,12 +3203,12 @@
False
True
False
Reload
True
-
+
+
+
+
+ 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__("[37m→[0m", 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