Check-in [ea81d1ad5d]
| Comment: | 2.0.8.5 |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA1: |
ea81d1ad5d247a6c1f2d305206a7eed5 |
| User & Date: | mario on 2012-01-09 03:45:04 |
| Other Links: | manifest | tags |
|
2012-01-09
| ||
| 03:45 | gnome help files check-in: c2c3526ac3 user: mario tags: trunk | |
| 03:45 | 2.0.8.5 check-in: ea81d1ad5d user: mario tags: trunk | |
| 03:42 | initial empty check-in check-in: 0732311dbe user: mario tags: trunk | |
Added action.py version [a384a5d7ee].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
#
# encoding: UTF-8
# api: streamtuner2
# type: functions
# title: play/record actions
# description: Starts audio applications, guesses MIME types for URLs
#
#
# Multimedia interface for starting audio players or browser.
#
#
# Each channel plugin has a .listtype which describes the linked
# audio playlist format. It's audio/x-scpls mostly, seldomly m3u,
# but sometimes url/direct if the entry[url] directly leads to the
# streaming server.
# As fallback there is a regex which just looks for URLs in the
# given resource (works for m3u/pls/xspf/asx/...). There is no
# actual url "filename" extension guessing.
#
#
#
import re
import os
import http
from config import conf
import platform
#from channels import __print__
def __print__(*args):
if conf.debug:
print(" ".join([str(a) for a in args]))
main = None
#-- media actions ---------------------------------------------
#
# implements "play" and "record" methods,
# but also "browser" for web URLs
#
class action:
# streamlink formats
lt = {"asx":"video/x-ms-asf", "pls":"audio/x-scpls", "m3u":"audio/x-mpegurl", "xspf":"application/xspf+xml", "href":"url/http", "ram":"audio/x-pn-realaudio", "smil":"application/smil"}
# media formats
mf = {"mp3":"audio/mp3", "ogg":"audio/ogg", "aac":"audio/aac"}
# web
@staticmethod
def browser(url):
__print__( conf.browser )
os.system(conf.browser + " '" + action.quote(url) + "' &")
# os shell cmd escaping
@staticmethod
def quote(s):
return "%r" % s
# calls player for stream url and format
@staticmethod
def play(url, audioformat="audio/mp3", listformat="text/x-href"):
if (url):
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 )
try:
action.run( action.interpol(cmd, url) )
except:
pass
@staticmethod
def run(cmd):
__print__( cmd )
os.system(cmd + (" &" if platform.system()!="Windows" else ""))
# streamripper
@staticmethod
def record(url, audioformat="audio/mp3", listformat="text/x-href", append="", row={}):
__print__( "record", url )
cmd = conf.record.get(audioformat, conf.record.get("*/*", None))
try: action.run( action.interpol(cmd, url, row) + append )
except: pass
# save as .m3u
@staticmethod
def save(row, fn, listformat="audio/x-scpls"):
# modify stream url
row["url"] = action.url(row["url"], listformat)
stream_urls = action.extract_urls(row["url"], listformat)
# output format
if (re.search("\.m3u", fn)):
txt = "#M3U\n"
for url in stream_urls:
txt += http.fix_url(url) + "\n"
# output format
elif (re.search("\.pls", fn)):
txt = "[playlist]\n" + "numberofentries=1\n"
for i,u in enumerate(stream_urls):
i = str(i + 1)
txt += "File"+i + "=" + u + "\n"
txt += "Title"+i + "=" + row["title"] + "\n"
txt += "Length"+i + "=-1\n"
txt += "Version=2\n"
# output format
elif (re.search("\.xspf", fn)):
txt = '<?xml version="1.0" encoding="UTF-8"?>' + "\n"
txt += '<?http header="Content-Type: application/xspf+xml" ?>' + "\n"
txt += '<playlist version="1" xmlns="http://xspf.org/ns/0/">' + "\n"
for attr,tag in [("title","title"), ("homepage","info"), ("playing","annotation"), ("description","annotation")]:
if row.get(attr):
txt += " <"+tag+">" + xmlentities(row[attr]) + "</"+tag+">\n"
txt += " <trackList>\n"
for u in stream_urls:
txt += ' <track><location>' + xmlentities(u) + '</location></track>' + "\n"
txt += " </trackList>\n</playlist>\n"
# output format
elif (re.search("\.json", fn)):
row["stream_urls"] = stream_urls
txt = str(row) # pseudo-json (python format)
# output format
elif (re.search("\.asx", fn)):
txt = "<ASX version=\"3.0\">\n" \
+ " <Title>" + xmlentities(row["title"]) + "</Title>\n" \
+ " <Entry>\n" \
+ " <Title>" + xmlentities(row["title"]) + "</Title>\n" \
+ " <MoreInfo href=\"" + row["homepage"] + "\"/>\n" \
+ " <Ref href=\"" + stream_urls[0] + "\"/>\n" \
+ " </Entry>\n</ASX>\n"
# output format
elif (re.search("\.smil", fn)):
txt = "<smil>\n<head>\n <meta name=\"title\" content=\"" + xmlentities(row["title"]) + "\"/>\n</head>\n" \
+ "<body>\n <seq>\n <audio src=\"" + stream_urls[0] + "\"/>\n </seq>\n</body>\n</smil>\n"
# unknown
else:
txt = ""
# write
if txt:
f = open(fn, "wb")
f.write(txt)
f.close()
pass
# replaces instances of %u, %l, %pls with urls / %g, %f, %s, %m, %m3u or local filenames
@staticmethod
def interpol(cmd, url, row={}):
# inject other meta fields
if row:
for field in row:
cmd = cmd.replace("%"+field, "%r" % row.get(field))
# add default if cmd has no %url placeholder
if cmd.find("%") < 0:
cmd = cmd + " %m3u"
# standard placeholders
if (re.search("%(url|pls|[ulr])", cmd)):
cmd = re.sub("%(url|pls|[ulr])", action.quote(url), cmd)
if (re.search("%(m3u|[fgm])", cmd)):
cmd = re.sub("%(m3u|[fgm])", action.quote(action.m3u(url)), cmd)
if (re.search("%(srv|[ds])", cmd)):
cmd = re.sub("%(srv|[ds])", action.quote(action.srv(url)), cmd)
return cmd
# eventually transforms internal URN/IRI to URL
@staticmethod
def url(url, listformat):
if (listformat == "audio/x-scpls"):
url = url
elif (listformat == "text/x-urn-streamtuner2-script"):
url = main.special.stream_url(url)
else:
url = url
return url
# download a .pls resource and extract urls
@staticmethod
def pls(url):
text = http.get(url)
__print__( "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
def srv(url):
return action.extract_urls(url)[0]
# retrieve real stream urls from .pls or .m3u links
@staticmethod
def extract_urls(pls, listformat="__not_used_yet__"):
# extract stream address from .pls URL
if (re.search("\.pls", pls)): #audio/x-scpls
return action.pls(pls)
elif (re.search("\.asx", pls)): #video/x-ms-asf
return re.findall("<Ref\s+href=\"(http://.+?)\"", http.get(pls))
elif (re.search("\.m3u|\.ram|\.smil", pls)): #audio/x-mpegurl
return re.findall("(http://[^\s]+)", http.get(pls), re.I)
else: # just assume it was a direct mp3/ogg streamserver link
return [ (pls if pls.startswith("/") else http.fix_url(pls)) ]
pass
# generate filename for temporary .m3u, if possible with unique id
@staticmethod
def tmp_fn(pls):
# use shoutcast unique stream id if available
stream_id = re.search("http://.+?/.*?(\d+)", pls, re.M)
stream_id = stream_id and stream_id.group(1) or "XXXXXX"
try:
channelname = main.current_channel
except:
channelname = "unknown"
return (conf.tmp+"/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):
return open(tmp_fn, "r").read().find("http://") > 0
# create a local .m3u file from it
@staticmethod
def m3u(pls):
# temp filename
(tmp_fn, unique) = action.tmp_fn(pls)
# 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 )
url_list = action.extract_urls(pls)
__print__( "urls=", url_list )
# output URL list to temporary .m3u file
if (len(url_list)):
#tmp_fn =
f = open(tmp_fn, "w")
f.write("#M3U\n")
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 )
raise "Empty PLS"
# open help browser
@staticmethod
def help(*args):
os.system("yelp /usr/share/doc/streamtuner2/help/ &")
#or action.browser("/usr/share/doc/streamtuner2/")
#class action
|
Added channels/__init__.py version [f76e54fd21].
> > > > > > > > > | 1 2 3 4 5 6 7 8 9 | # # encoding: UTF-8 # api: python # type: R # from channels._generic import * from channels._generic import __print__ |
Added channels/_generic.py version [e213956f5f].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 |
#
# encoding: UTF-8
# api: streamtuner2
# type: class
# title: channel objects
# description: base functionality for channel modules
# version: 1.0
# author: mario
# license: public domain
#
#
# GenericChannel implements the basic GUI functions and defines
# the default channel data structure. It implements base and
# fallback logic for all other channel implementations.
#
# Built-in channels derive directly from generic. Additional
# channels don't have a pre-defined Notebook tab in the glade
# file. They derive from the ChannelPlugins class instead, which
# adds the required gtk Widgets manually.
#
import gtk
from mygtk import mygtk
from config import conf
import http
import action
import favicon
import os.path
import xml.sax.saxutils
import re
import copy
# dict==object
class struct(dict):
def __init__(self, *xargs, **kwargs):
self.__dict__ = self
self.update(kwargs)
[self.update(x) for x in xargs]
pass
# generic channel module ---------------------------------------
class GenericChannel(object):
# desc
api = "streamtuner2"
module = "generic"
title = "GenericChannel"
version = 1.0
homepage = "http://milki.inlcude-once.org/streamtuner2/"
base_url = ""
listformat = "audio/x-scpls"
audioformat = "audio/mp3" # fallback value
config = []
# categories
categories = ["empty", ]
current = ""
default = "empty"
shown = None # last selected entry in stream list, also indicator if notebook tab has been selected once / stream list of current category been displayed yet
# gui + data
streams = {} #meta information dicts
liststore = {} #gtk data structure
gtk_list = None #gtk widget
gtk_cat = None #gtk widget
# 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}], ],
["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}], ],
[False, 0, ["---format", str, None, {}], ],
[False, 0, ["favourite", bool, None, {}], ],
[False, 0, ["deleted", bool, None, {}], ],
[False, 0, ["search_col", str, None, {}], ],
[False, 0, ["search_set", bool, None, {}], ],
]
rowmap = [] # [state,genre,title,...] field enumeration still needed separately
titles = {} # for easier adapting of column titles in datamap
# regex
rx_www_url = re.compile("""(www(\.\w+[\w-]+){2,}|(\w+[\w-]+[ ]?\.)+(com|FM|net|org|de|PL|fr|uk))""", re.I)
# constructor
def __init__(self, parent=None):
#self.streams = {}
self.gtk_list = None
self.gtk_cat = None
# only if streamtuner2 is run in graphical mode
if (parent):
self.cache()
self.gui(parent)
pass
# called before application shutdown
# some plugins might override this, to save their current streams[] data
def shutdown(self):
pass
#__del__ = shutdown
# returns station entries from streams[] for .current category
def stations(self):
return self.streams.get(self.current, [])
def rowno(self):
pass
def row(self):
pass
# read previous channel/stream data, if there is any
def cache(self):
# stream list
cache = conf.load("cache/" + self.module)
if (cache):
self.streams = cache
# categories
cache = conf.load("cache/categories_" + self.module)
if (cache):
self.categories = cache
pass
# initialize Gtk widgets / data objects
def gui(self, parent):
#print(self.module + ".gui()")
# save reference to main window/glade API
self.parent = parent
self.gtk_list = parent.get_widget(self.module+"_list")
self.gtk_cat = parent.get_widget(self.module+"_cat")
# 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():
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)):
self.rowmap.append(row[x][0])
# load default category
if (self.current):
self.load(self.current)
else:
mygtk.columns(self.gtk_list, self.datamap, [{}])
# add to main menu
mygtk.add_menu(parent.channelmenuitems, self.title, lambda w: parent.channel_switch(w, self.module) or 1)
# make private copy of .datamap and modify field (title= only ATM)
def update_datamap(self, search="name", title=None):
if self.datamap == GenericChannel.datamap:
self.datamap = copy.deepcopy(self.datamap)
for i,row in enumerate(self.datamap):
if row[2][0] == search:
row[0] = title
# switch stream category,
# 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)):
new_streams = self.update_streams(category)
if new_streams:
# modify
[self.postprocess(row) for row in new_streams]
# don't lose forgotten streams
if conf.retain_deleted:
self.streams[category] = new_streams + self.deleted_streams(new_streams, self.streams.get(category,[]))
else:
self.streams[category] = new_streams
# save in cache
self.save()
# invalidate gtk list cache
#if (self.liststore.has_key(category)):
# del self.liststore[category]
else:
# parse error
self.parent.status("category parsed empty.")
self.streams[category] = [{"title":"no contents found on directory server","bitrate":0,"max":0,"listeners":0,"playing":"error","favourite":0,"deleted":0}]
print("oooops, parser returned nothing for category " + category)
# assign to treeview model
#self.streams[self.default] = []
#if (self.liststore.has_key(category)): # was already loded before
# self.gtk_list.set_model(self.liststore[category])
#else: # currently list is new, had not been converted to gtk array before
# self.liststore[category] = \
mygtk.do(lambda:mygtk.columns(self.gtk_list, self.datamap, self.prepare(self.streams[category])))
# set pointer
self.current = category
pass
# store current streams data
def save(self):
conf.save("cache/" + self.module, self.streams, gz=1)
# called occasionally while retrieving and parsing
def update_streams_partially_done(self, entries):
mygtk.do(lambda: mygtk.columns(self.gtk_list, self.datamap, entries))
# 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)):
row["deleted"] = 1
diff.append(row)
return diff
# prepare data for display
def prepare(self, streams):
for i,row in enumerate(streams):
# 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"))):
streams[i]["favourite"] = 1
# state icon: INFO or DELETE
if (not row.get("state")):
if row.get("favourite"):
streams[i]["state"] = gtk.STOCK_ABOUT
if conf.retain_deleted and row.get("deleted"):
streams[i]["state"] = gtk.STOCK_DELETE
# guess homepage url
#self.postprocess(row)
# favicons?
if conf.show_favicons:
homepage_url = row.get("homepage")
# check for availability of PNG file, inject local icons/ filename
if homepage_url and favicon.available(homepage_url):
streams[i]["favicon"] = favicon.file(homepage_url)
return streams
# data preparations directly after reload
def postprocess(self, row):
# remove non-homepages from shoutcast
if row.get("homepage") and row["homepage"].find("//yp.shoutcast.")>0:
row["homepage"] = ""
# deduce homepage URLs from title
# by looking for www.xyz.com domain names
if not row.get("homepage"):
url = self.rx_www_url.search(row.get("title", ""))
if url:
url = url.group(0).lower().replace(" ", "")
url = (url if url.find("www.") == 0 else "www."+url)
row["homepage"] = http.fix_url(url)
return row
# reload current stream from web directory
def reload(self):
self.load(self.current, force=1)
def switch(self):
self.load(self.current, force=0)
# display .current category, once notebook/channel tab is first opened
def first_show(self):
print("first_show ", self.module)
print 1
print self.shown
if (self.shown != 55555):
print 2
# if category tree is empty, initialize it
if not self.categories:
print 3
#self.parent.thread(self.reload_categories)
print("reload categories");
self.reload_categories()
self.display_categories()
self.current = self.categories.keys()[0]
print self.current
self.load(self.current)
# load current category
else:
print 4
self.load(self.current)
# put selection/cursor on last position
try:
print 5
self.gtk_list.get_selection().select_path(self.shown)
except:
pass
# this method will only be invoked once
self.shown = 55555
# update categories, save, and display
def reload_categories(self):
# get data and save
self.update_categories()
conf.save("cache/categories_"+self.module, self.categories)
# display outside of this non-main thread
mygtk.do(self.display_categories)
# insert content into gtk category list
def display_categories(self):
# remove any existing columns
if self.gtk_cat:
[self.gtk_cat.remove_column(c) for c in self.gtk_cat.get_columns()]
# rebuild gtk.TreeView
mygtk.tree(self.gtk_cat, self.categories, title="Category", icon=gtk.STOCK_OPEN);
# if it's a short list of categories, there's probably subfolders
if len(self.categories) < 20:
self.gtk_cat.expand_all()
# select any first element
self.gtk_cat.get_selection().select_path("0") #set_cursor
self.currentcat()
# selected category
def currentcat(self):
(model, iter) = self.gtk_cat.get_selection().get_selected()
if (type(iter) == gtk.TreeIter):
self.current = model.get_value(iter, 0)
return self.current
#--------------------------- actions ---------------------------------
# invoke action.play,
# can be overridden to provide channel-specific "play" alternative
def play(self, row):
if row.get("url"):
# parameters
audioformat = row.get("format", self.audioformat)
listformat = row.get("listformat", self.listformat)
# invoke audio player
action.action.play(row["url"], audioformat, listformat)
#--------------------------- utility functions -----------------------
# remove html <tags> from string
def strip_tags(self, s):
return re.sub("<.+?>", "", s)
# convert audio format nick/shortnames to mime types, e.g. "OGG" to "audio/ogg"
def mime_fmt(self, s):
# clean string
s = s.lower().strip()
# rename
map = {
"audio/mpeg":"audio/mp3", # Note the real mime type is /mpeg, but /mp3 is more understandable in the GUI
"ogg":"ogg", "ogm":"ogg", "xiph":"ogg", "vorbis":"ogg", "vnd.xiph.vorbis":"ogg",
"mpeg":"mp3", "mp":"mp3", "mp2":"mp3", "mpc":"mp3", "mps":"mp3",
"aac+":"aac", "aacp":"aac",
"realaudio":"x-pn-realaudio", "real":"x-pn-realaudio", "ra":"x-pn-realaudio", "ram":"x-pn-realaudio", "rm":"x-pn-realaudio",
# yes, we do video
"flv":"video/flv", "mp4":"video/mp4",
}
map.update(action.action.lt) # list type formats (.m3u .pls and .xspf)
if map.get(s):
s = map[s]
# add prefix:
if s.find("/") < 1:
s = "audio/" + s
#
return s
# remove SGML/XML entities
def entity_decode(self, s):
return xml.sax.saxutils.unescape(s)
# convert special characters to &xx; escapes
def xmlentities(self, s):
return xml.sax.saxutils.escape(s)
# channel plugin without glade-pre-defined notebook tab
#
class ChannelPlugin(GenericChannel):
module = "abstract"
title = "New Tab"
version = 0.1
def gui(self, parent):
# name id
module = self.module
if parent:
# two panes
vbox = gtk.HPaned()
vbox.show()
# category treeview
sw1 = gtk.ScrolledWindow()
sw1.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
sw1.set_property("width_request", 150)
sw1.show()
tv1 = gtk.TreeView()
tv1.set_property("width_request", 75)
tv1.set_property("enable_tree_lines", True)
tv1.connect("button_release_event", parent.on_category_clicked)
tv1.show()
sw1.add(tv1)
vbox.pack1(sw1, resize=False, shrink=True)
# stream list
sw2 = gtk.ScrolledWindow()
sw2.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
sw2.show()
tv2 = gtk.TreeView()
tv2.set_property("width_request", 200)
tv2.set_property("enable_tree_lines", True)
tv2.connect("row_activated", parent.on_play_clicked)
tv2.show()
sw2.add(tv2)
vbox.pack2(sw2, resize=True, shrink=True)
# prepare label
label = gtk.HBox()
label.set_property("visible", True)
fn = "/usr/share/streamtuner2/channels/" + self.module + ".png"
if os.path.exists(fn):
icon = gtk.Image()
icon.set_property("pixbuf", gtk.gdk.pixbuf_new_from_file(fn))
icon.set_property("icon-size", 1)
icon.set_property("visible", True)
label.pack_start(icon, expand=False, fill=True)
if self.title:
text = gtk.Label(self.title)
text.set_property("visible", True)
label.pack_start(text, expand=True, fill=True)
# pack it into an event container to catch double-clicks
ev_label = gtk.EventBox()
ev_label.add(label)
ev_label.connect('event', parent.on_homepage_channel_clicked)
# add notebook tab
tab = parent.notebook_channels.append_page(vbox, ev_label)
# to widgets
self.gtk_cat = tv1
parent.widgets[module + "_cat"] = tv1
self.gtk_list = tv2
parent.widgets[module + "_list"] = tv2
parent.widgets["v_" + module] = vbox
parent.widgets["c_" + module] = ev_label
tv2.connect('button-press-event', parent.station_context_menu)
# double-click catch
# add module to list
#parent.channels[module] = None
#parent.channel_names.append(module)
""" -> already taken care of in main.load_plugins() """
# superclass
GenericChannel.gui(self, parent)
# wrapper for all print statements
def __print__(*args):
if conf.debug:
print(" ".join([str(a) for a in args]))
__debug_print__ = __print__
|
Added channels/basicch.py version [2ad035d297].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# 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"""
<a\shref="shows.cfm[?]showid=(\d+)">([^<>]+)</a>
\s*<span\sclass="smalltxt">([^<>]+)</span><br
""", re.S|re.X)
#-- archived shows
for uu in rx_current.findall(html):
(id, title, genre) = uu
self.catmap[title] = {
"sub_url": self.base + "shows.cfm?showid=" + id,
"title": title,
"genre": genre,
"id": id,
}
#-- populate category treee
self.categories = [
"METAMIX",
"shows", [ title for title in self.catmap.keys() ]
]
#-- 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):
rx_metamix = re.compile("""
<a\shref="(http://[^<">]+)">(METAMIX[^<]+)</a>
\s+<span\sclass="smalltxt">([^<>]+)</span><br
""", re.S|re.X)
rx_playlist = re.compile("""
<filename>(http://[^<">]+)</filename>\s*
<artist>([^<>]+)</artist>\s*
<title>([^<>]+)</title>
""", 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]+)'[^>]+>
<b>(\w+[^<>]+)</.+?
<b>(\w+[^<>]+)</.+?
<a\s+href="(http://[^">]+)"
""", re.S|re.X)
rx_archive = re.compile("""
href="javascript:openWindow.'([\w.?=\d]+)'[^>]+>.+?
color="000000">(\w+[^<>]+)</.+?
color="000000">(\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)</font>.+?
href="(http://[^">]+|/ram/\w+.ram)"[^>]*>([^<>]+)</a>
.+? (>(\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/file.py version [13559afbce].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
#
# api: streamtuner2
# title: file browser plugin
# description: browses through local files, displays mp3/oggs and m3u/pls files
# version: -0.5
# depends: mutagen, kiwi
# x:
# x:
# x:
# x:
#
#
# Local file browser.
#
#
# modules
import os
import re
from channels import *
from config import conf
# ID3 libraries
try:
from mutagen import File as get_meta
except:
try:
print("just basic ID3 support")
from ID3 import ID3
get_meta = lambda fn: dict([(k.lower(),v) for k,v in ID3(fn).iteritems()])
except:
print("you are out of luck in regards to mp3 browsing")
get_meta = lambda *x: {}
# work around mutagens difficult interface
def mutagen_postprocess(d):
if d.get("TIT2"):
return {
"encoder": d["TENC"][0],
"title": d["TIT2"][0],
"artist": d["TPE1"][0],
# "tyer?????????????": d["TYER"][0],
# "track": d["TRCK"][0],
"album": d["TALB"][0],
}
else:
return d
# file browser / mp3 directory listings
class file (ChannelPlugin):
# info
api = "streamtuner2"
module = "file"
title = "file browser"
version = -0.5
listtype = "url/file"
# data
config = [
{"name":"file_browser_dir", "type":"text", "value":os.environ["HOME"]+"/Music, /media/music", "description":"list of directories to scan for audio files"},
{"name":"file_browser_ext", "type":"text", "value":"mp3,ogg, m3u,pls,xspf, avi,flv,mpg,mp4", "description":"file type filter"},
]
streams = {}
categories = []
dir = []
ext = []
# display
datamap = [ # coltitle width [ datasrc key, type, renderer, attrs ] [cellrenderer2], ...
["", 20, ["state", str, "pixbuf", {}], ],
["Genre", 65, ['genre', str, "t", {"editable":8}], ],
["File", 160, ["filename", str, "t", {"strikethrough":11, "cell-background":12, "cell-background-set":13}], ],
["Title", 205, ["title", str, "t", {"editable":8}], ],
["Artist", 125, ["artist", str, "t", {"editable":8}], ],
["Album", 125, ["album", str, "t", {"editable":8}], ],
["Bitrate", 35, ["bitrate", int, "t", {}], ],
["Format", 50, ["format", str, None, {}], ],
[False, 0, ["editable", bool, None, {}], ],
[False, 0, ["favourite", bool, None, {}], ],
[False, 0, ["deleted", bool, None, {}], ],
[False, 0, ["search_col", str, None, {}], ],
[False, 0, ["search_set", bool, None, {}], ],
]
rowmap = []
# prepare
def __init__(self, parent):
# data dirs
self.dir = [s.strip() for s in conf.file_browser_dir.split(",")]
self.ext = [s.strip() for s in conf.file_browser_ext.split(",")]
# first run
if not self.categories or not self.streams:
self.scan_dirs()
# draw gtk lists
ChannelPlugin.__init__(self, parent)
# make editable
#{editable:8}
# add custom context menu
#self.gtk_list.connect('button-press-event', self.context_menu)
# save list?
#save = lambda *x: None
# yeah, give it a try
# don't load cache file
cache = lambda *x: None
# read dirs
def scan_dirs(self):
self.categories = []
# add main directory
for main in self.dir:
if os.path.exists(main):
self.categories.append(main)
# prepare subdirectories list
sub = []
self.categories.append(sub)
# look through
for dir, subdirs, files in os.walk(main):
name = os.path.basename(dir)
while name in self.categories:
name = name + "2"
# files in subdir
if files:
sub.append(name)
self.streams[name] = [self.file_entry(fn, dir) for fn in files if self.we_like_that_extension(fn)]
# plant a maindir reference to shortname
self.streams[main] = self.streams[os.path.basename(main)]
# extract meta data
def file_entry(self, fn, dir):
# basic data
meta = {
"title": fn,
"filename": fn,
"url": dir + "/" + fn,
"genre": "",
"format": self.mime_fmt(fn[-3:]),
"editable": True,
}
# add ID3
meta.update(mutagen_postprocess(get_meta(dir + "/" + fn) or {}))
return meta
# check fn for .ext
def we_like_that_extension(self, fn):
return fn[-3:] in self.ext
# same as init
def update_categories(self):
self.scan_dirs()
# same as init
def update_streams(self, cat, x=0):
self.scan_dirs()
return self.streams.get(os.path.basename(cat))
|
Added channels/global_key.py version [71e77fdc54].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
#
# type: feature
# api: streamtuner2
# title: global keyboard shortcut
# description: allows switching radios in bookmarks list via key press
# version: 0.2
# depends: python-keybinder
#
#
# Binds a key to global desktop (F13 = left windows key). On keypress
# it switches the currently playing radio station to another one in
# bookmarks list.
#
# Valid key names are for example F9, <Ctrl>G, <Alt>R, <Super>N
#
import keybinder
from config import conf
import action
import random
# register a key
class global_key(object):
module = "global_key"
title = "keyboard shortcut"
version = 0.2
config = [
dict(name="switch_key", type="text", value="XF86Forward", description="global key for switching radio"),
dict(name="switch_channel", type="text", value="bookmarks:favourite", description="station list to alternate in"),
dict(name="switch_random", type="boolean", value=0, description="pick random channel, instead of next"),
]
last = 0
# register
def __init__(self, parent):
self.parent = parent
try:
for i,keyname in enumerate(conf.switch_key.split(",")): # allow multiple keys
keybinder.bind(keyname, self.switch, ((-1 if i else +1))) # forward +1 or backward -1
except:
print("Key could not be registered")
# key event
def switch(self, num, *any):
# bookmarks, favourite
channel, cat = conf.switch_channel.split(":")
# get list
streams = self.parent.channels[channel].streams[cat]
# pickrandom
if conf.switch_random:
self.last = random.randint(0, len(streams)-1)
# or iterate over list
else:
self.last = self.last + num
if self.last >= len(streams):
self.last = 0
elif self.last < 0:
self.last = len(streams)-1
# play
i = self.last
action.action.play(streams[i]["url"], streams[i]["format"])
# set pointer in gtk.TreeView
if self.parent.channels[channel].current == cat:
self.parent.channels[channel].gtk_list.get_selection().select_path(i)
|
Added channels/google.py version [3f0788927c].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
#
# encoding: ISO-8859-1
# api: streamtuner2
# title: google stations
# description: Looks up web radio stations from DMOZ/Google directory
# depends: channels, re, http
# version: 0.1
# author: Mario, original: Jean-Yves Lefort
#
# This is a plugun from streamtuner1. It has been rewritten for the
# more mundane plugin API of streamtuner2 - reimplementing ST seemed
# to much work.
# Also it has been rewritten to query DMOZ directly. Google required
# the use of fake User-Agents for access, and the structure on DMOZ
# is simpler (even if less HTML-compliant). DMOZ probably is kept
# more up-to-date as well.
# PS: we need to check out //musicmoz.org/
#
# Copyright (c) 2003, 2004 Jean-Yves Lefort
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of Jean-Yves Lefort nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re, os, gtk
from channels import *
from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities
import http
### constants #################################################################
GOOGLE_DIRECTORY_ROOT = "http://www.dmoz.org"
CATEGORIES_URL_POSTFIX = "/Arts/Music/Sound_Files/MP3/Streaming/Stations/"
#GOOGLE_DIRECTORY_ROOT = "http://directory.google.com"
#CATEGORIES_URL_POSTFIX = "/Top/Arts/Music/Sound_Files/MP3/Streaming/Stations/"
GOOGLE_STATIONS_HOME = GOOGLE_DIRECTORY_ROOT + CATEGORIES_URL_POSTFIX
"""<li><a href="/Arts/Music/Sound_Files/MP3/Streaming/Stations/Jazz/"><b>Jazz</b></a>"""
re_category = re.compile('<a href="(.+)">(<b>)([^:]+?)(</b>)</a>', re.I|re.M)
#re_stream = re.compile('^<td><font face="arial,sans-serif"><a href="(.*)">(.*)</a>')
#re_description = re.compile('^<br><font size=-1> (.*?)</font>')
"""<li><a href="http://www.atlantabluesky.com/">Atlanta Blue Sky</a> - Rock and alternative streaming audio. Live real-time requests."""
re_stream_desc = re.compile('^<li><a href="(.*)">([^<>]+)</a>( - )?([^<>\n\r]+)', re.M|re.I)
######
# Google Stations is actually now DMOZ Stations
class google(ChannelPlugin):
# description
title = "Google"
module = "google"
homepage = GOOGLE_STATIONS_HOME
version = 0.2
# config data
config = [
# {"name": "theme", "type": "text", "value":"Tactile", "description":"Streamtuner2 theme; no this isn't a google-specific option. But you know, the plugin options are a new toy."},
# {"name": "flag2", "type": "boolean", "value":1, "description":"oh see, an unused checkbox"}
]
# category map
categories = ['Google/DMOZ Stations', 'Alternative', 'Ambient', 'Classical', 'College', 'Country', 'Dance', 'Experimental', 'Gothic', 'Industrial', 'Jazz', 'Local', 'Lounge', 'Metal', 'New Age', 'Oldies', 'Old-Time Radio', 'Pop', 'Punk', 'Rock', '80s', 'Soundtracks', 'Talk', 'Techno', 'Urban', 'Variety', 'World']
catmap = [('Google/DMOZ Stations', '__main', '/Arts/Radio/Internet/'), ['Alternative', 'Alternative', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Alternative/'], ['Ambient', 'Ambient', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Ambient/'], ['Classical', 'Classical', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Classical/'], ['College', 'College', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/College/'], ['Country', 'Country', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Country/'], ['Dance', 'Dance', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Dance/'], ['Experimental', 'Experimental', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Experimental/'], ['Gothic', 'Gothic', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Gothic/'], ['Industrial', 'Industrial', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Industrial/'], ['Jazz', 'Jazz', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Jazz/'], ['Local', 'Local', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Local/'], ['Lounge', 'Lounge', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Lounge/'], ['Metal', 'Metal', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Metal/'], ['New Age', 'New Age', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/New_Age/'], ['Oldies', 'Oldies', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Oldies/'], ['Old-Time Radio', 'Old-Time Radio', '/Arts/Radio/Formats/Old-Time_Radio/Streaming_MP3_Stations/'], ['Pop', 'Pop', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Pop/'], ['Punk', 'Punk', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Punk/'], ['Rock', 'Rock', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/'], ['80s', '80s', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/80s/'], ['Soundtracks', 'Soundtracks', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Soundtracks/'], ['Talk', 'Talk', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Talk/'], ['Techno', 'Techno', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Dance/Techno/'], ['Urban', 'Urban', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Urban/'], ['Variety', 'Variety', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Variety/'], ['World', 'World', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/World/']]
#def __init__(self, parent):
# #self.update_categories()
# ChannelPlugin.__init__(self, parent)
# refresh category list
def update_categories(self):
# interim data structure for categories (label, google-id/name, url)
categories = [
("Google/DMOZ Stations", "__main", "/Arts/Radio/Internet/"),
]
# fetch and extract list
html = http.get(GOOGLE_DIRECTORY_ROOT + CATEGORIES_URL_POSTFIX)
for row in re_category.findall(html):
if row:
name = entity_decode(row[2])
label = name
href = entity_decode(row[0])
if href[0] != "/":
href = CATEGORIES_URL_POSTFIX + href
categories.append([label, name, href])
# return
self.catmap = categories
self.categories = [x[0] for x in categories]
pass
# actually saving this into _categories and _catmap.json would be nice
# ...
# download links from dmoz listing
def update_streams(self, cat, force=0):
# result list
ls = []
# get //dmoz.org/HREF for category name
try:
(label, name, href) = [x for x in self.catmap if x[0]==cat][0]
except:
return ls # wrong category
# download
html = http.get(GOOGLE_DIRECTORY_ROOT + href)
# filter
for row in re_stream_desc.findall(html):
if row:
row = {
"homepage": entity_decode(row[0]),
"title": entity_decode(row[1]),
"playing": entity_decode(row[3]),
}
ls.append(row)
# final list for current category
return ls
|
Added channels/internet_radio_org_uk.py version [5a7ebd0c8e].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
#
# api: streamtuner2
# title: internet-radio.org.uk
# description: io channel
# version: 0.1
#
#
# Might become new main plugin
#
#
#
from channels import *
import re
from config import conf
import http
from pq import pq
# streams and gui
class internet_radio_org_uk (ChannelPlugin):
# description
title = "InternetRadio"
module = "internet_radio_org_uk"
homepage = "http://www.internet-radio.org.uk/"
version = 0.1
listformat = "audio/x-scpls"
# settings
config = [
{"name":"internetradio_max_pages", "type":"int", "value":5, "description":"How many pages to fetch and read."},
]
# category map
categories = []
current = ""
default = ""
# load genres
def update_categories(self):
html = http.get(self.homepage)
rx = re.compile("""<option[^>]+value="/stations/[-+&.\w\s%]+/">([^<]+)</option>""")
self.categories = rx.findall(html)
# fetch station lists
def update_streams(self, cat, force=0):
entries = []
if cat not in self.categories:
return []
# regex
#rx_div = re.compile('<tr valign="top" class="stream">(.+?)</tr>', re.S)
rx_data = re.compile("""
(?:M3U|PLS)',\s*'(http://[^']+)'
.*?
<br><br>([^\n]*?)</td>
.*?
(?:href="(http://[^"]+)"[^>]+target="_blank"[^>]*)?
>\s*
<b>\s*(\w[^<]+)[<\n]
.*?
playing\s*:\s*([^<\n]+)
.*?
(\d+)\s*Kbps
(?:<br>(\d+)\s*Listeners)?
""", re.S|re.X)
#rx_homepage = re.compile('href="(http://[^"]+)"[^>]+target="_blank"')
rx_pages = re.compile('href="/stations/[-+\w%\d\s]+/page(\d+)">\d+</a>')
rx_numbers = re.compile("(\d+)")
self.parent.status("downloading category pages...")
# multiple pages
page = 1
max = int(conf.internetradio_max_pages)
max = (max if max > 1 else 1)
while page <= max:
# fetch
html = http.get(self.homepage + "stations/" + cat.lower().replace(" ", "%20") + "/" + ("page"+str(page) if page>1 else ""))
# regex parsing?
if not conf.pyquery:
# step through
for uu in rx_data.findall(html):
(url, genre, homepage, title, playing, bitrate, listeners) = uu
# transform data
entries.append({
"url": url,
"genre": self.strip_tags(genre),
"homepage": http.fix_url(homepage),
"title": title,
"playing": playing,
"bitrate": int(bitrate),
"listeners": int(listeners if listeners else 0),
"format": "audio/mp3", # there is no stream info on that, but internet-radio.org.uk doesn't seem very ogg-friendly anyway, so we assume the default here
})
# DOM parsing
else:
# the streams are arranged in table rows
doc = pq(html)
for dir in (pq(e) for e in doc("tr.stream")):
bl = dir.find("td[align=right]").text()
bl = rx_numbers.findall(str(bl) + " 0 0")
entries.append({
"title": dir.find("b").text(),
"homepage": http.fix_url(dir.find("a.url").attr("href")),
"url": dir.find("a").eq(2).attr("href"),
"genre": dir.find("td").eq(0).text(),
"bitrate": int(bl[0]),
"listeners": int(bl[1]),
"format": "audio/mp3",
"playing": dir.find("td").eq(1).children().remove().end().text()[13:].strip(),
})
# next page?
if str(page+1) not in rx_pages.findall(html):
max = 0
else:
page = page + 1
# keep listview updated while searching
self.update_streams_partially_done(entries)
try: self.parent.status(float(page)/float(max))
except: """there was a div by zero bug report despite max=1 precautions"""
# fin
self.parent.status()
return entries
|
Added channels/jamendo.py version [0df05c5caf].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# api: streamtuner2
# title: jamendo browser
#
# For now this is really just a browser, doesn't utilizt the jamendo API yet.
# Requires more rework of streamtuner2 list display to show album covers.
#
import re
import http
from config import conf
from channels import *
from xml.sax.saxutils import unescape
# jamendo CC music sharing site
class jamendo (ChannelPlugin):
# description
title = "Jamendo"
module = "jamendo"
homepage = "http://www.jamendo.com/"
version = 0.2
base = "http://www.jamendo.com/en/"
listformat = "url/http"
categories = [] #"top 100", "reload category tree!", ["menu > channel > reload.."]]
titles = dict( title="Artist", playing="Album/Song", bitrate=False, listeners=False )
config = [
{"name":"jamendo_stream_format", "value":"ogg2", "type":"text", "description":"streaming format, 'ogg2' or 'mp31'"}
]
# refresh category list
def update_categories(self):
html = http.get(self.base + "tags")
rx_current = re.compile(r"""
<a\s[^>]+rel="tag"[^>]+href="(http://www.jamendo.com/\w\w/tag/[\w\d]+)"[^>]*>([\w\d]+)</a>
""", re.S|re.X)
#-- categories
tags = []
for uu in rx_current.findall(html):
(href, title) = uu
tags.append(title)
self.categories = [
"top 100",
"radios",
"tags", tags
]
# download links from dmoz listing
def update_streams(self, cat, force=0):
entries = []
# top list
if cat == "top" or cat == "top 100":
html = http.get(self.base + "top")
rx_top = re.compile("""
<img[^>]+src="(http://imgjam.com/albums/[\w\d]+/\d+/covers/1.\d+.jpg)"
.*?
<a\stitle="([^"]+)\s*-\s*([^"]+)"\s+class="track_name"\s+href="(http://www.jamendo.com/\w+/track/(\d+))"
""", re.X|re.S)
for uu in rx_top.findall(html):
(cover, title, artist, track, track_id) = uu
entries.append({
"title": artist,
"playing": title,
"homepage": track,
"url": self.track_url(track_id, conf.jamendo_stream_format),
"favicon": self.cover(cover),
"format": self.stream_mime(),
})
# static
elif cat == "radios":
rx = '>(\w+[-/\s]*\w+)</a>.+?/(get2/stream/track/m3u/radio_track_inradioplaylist/[?]order=numradio_asc&radio_id=\d+)"'
for uu in re.findall(rx, http.get(self.base + "radios")):
(name, url) = uu
entries.append({
"title": name,
"url": self.homepage,
"homepage": self.base + "radios",
})
# genre list
else:
html = http.get(self.base + "tag/" + cat)
rx_tag = re.compile("""
<a\s+title="([^"]+)\s*-\s*([^"]+)"
\s+href="(http://www.jamendo.com/\w+/album/(\d+))"\s*>
\s*<img[^>]+src="(http://imgjam.com/albums/[\w\d]+/\d+/covers/1.\d+.jpg)"
.*? /tag/([\w\d]+)"
""", re.X|re.S)
for uu in rx_tag.findall(html):
(artist, title, album, album_id, cover, tag) = uu
entries.append({
"title": artist,
"playing": title,
"homepage": album,
"url": self.track_url(album_id, conf.jamendo_stream_format, "album"),
"favicon": self.cover(cover),
"genre": tag,
"format": self.stream_mime(),
})
# done
return entries
# smaller album link
def cover(self, url):
return url.replace(".100",".50").replace(".130",".50")
# track id to download url
def track_url(self, track_id, fmt="ogg2", track="track", urltype="redirect"):
# track = "album"
# fmt = "mp31"
# urltype = "m3u"
return "http://api.jamendo.com/get2/stream/"+track+"/"+urltype+"/?id="+track_id+"&streamencoding="+fmt
# audio/*
def stream_mime(self):
if conf.jamendo_stream_format.find("og") >= 0:
return "audio/ogg"
else:
return "audio/mp3"
|
Added channels/links.py version [692f83efe7].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
#
# api: streamtuner2
# title: links to directory services
# description: provides a simple list of homepages for directory services
# version: 0.1
# priority: rare
#
#
# Simply adds a "links" entry in bookmarks tab, where known channels
# and some others are listed with homepage links.
#
#
from channels import *
import copy
# hooks into main.bookmarks
class links (object):
# plugin info
module = "links"
title = "Links"
version = 0.1
# configuration settings
config = [ ]
# list
streams = [ ]
default = {
"radio.de": "http://www.radio.de/",
"musicgoal": "http://www.musicgoal.com/",
"streamfinder": "http://www.streamfinder.com/",
"last.fm": "http://www.last.fm/",
"rhapsody (US-only)": "http://www.rhapsody.com/",
"pandora (US-only)": "http://www.pandora.com/",
"radiotower": "http://www.radiotower.com/",
"pirateradio": "http://www.pirateradionetwork.com/",
"R-L": "http://www.radio-locator.com/",
"radio station world": "http://radiostationworld.com/",
"surfmusik.de": "http://www.surfmusic.de/",
}
# prepare gui
def __init__(self, parent):
if parent:
# target channel
bookmarks = parent.bookmarks
if not bookmarks.streams.get(self.module):
bookmarks.streams[self.module] = []
bookmarks.add_category(self.module)
# collect links from channel plugins
for name,channel in parent.channels.iteritems():
try:
self.streams.append({
"favourite": 1,
"title": channel.title,
"homepage": channel.homepage,
})
except: pass
for title,homepage in self.default.iteritems():
self.streams.append({
"title": title,
"homepage": homepage,
})
# add to bookmarks
bookmarks.streams[self.module] = self.streams
|
Added channels/live365.py version [0058898af0].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# streamtuner2 modules
from config import conf
from mygtk import mygtk
import http
from channels import *
from channels import __print__
# python modules
import re
import xml.dom.minidom
from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities
import gtk
import copy
import urllib
# channel live365
class live365(ChannelPlugin):
# desc
api = "streamtuner2"
module = "live365"
title = "Live365"
version = 0.1
homepage = "http://www.live365.com/"
base_url = "http://www.live365.com/"
listformat = "url/http"
mediatype = "audio/mpeg"
# content
categories = ['Alternative', ['Britpop', 'Classic Alternative', 'College', 'Dancepunk', 'Dream Pop', 'Emo', 'Goth', 'Grunge', 'Indie Pop', 'Indie Rock', 'Industrial', 'Lo-Fi', 'Modern Rock', 'New Wave', 'Noise Pop', 'Post-Punk', 'Power Pop', 'Punk'], 'Blues', ['Acoustic Blues', 'Chicago Blues', 'Contemporary Blues', 'Country Blues', 'Delta Blues', 'Electric Blues', 'Cajun/Zydeco'], 'Classical', ['Baroque', 'Chamber', 'Choral', 'Classical Period', 'Early Classical', 'Impressionist', 'Modern', 'Opera', 'Piano', 'Romantic', 'Symphony'], 'Country', ['Alt-Country', 'Americana', 'Bluegrass', 'Classic Country', 'Contemporary Bluegrass', 'Contemporary Country', 'Honky Tonk', 'Hot Country Hits', 'Western'], 'Easy Listening', ['Exotica', 'Lounge', 'Orchestral Pop', 'Polka', 'Space Age Pop'], 'Electronic/Dance', ['Acid House', 'Ambient', 'Big Beat', 'Breakbeat', 'Disco', 'Downtempo', "Drum 'n' Bass", 'Electro', 'Garage', 'Hard House', 'House', 'IDM', 'Jungle', 'Progressive', 'Techno', 'Trance', 'Tribal', 'Trip Hop'], 'Folk', ['Alternative Folk', 'Contemporary Folk', 'Folk Rock', 'New Acoustic', 'Traditional Folk', 'World Folk'], 'Freeform', ['Chill', 'Experimental', 'Heartache', 'Love/Romance', 'Music To ... To', 'Party Mix', 'Patriotic', 'Rainy Day Mix', 'Reality', 'Shuffle/Random', 'Travel Mix', 'Trippy', 'Various', 'Women', 'Work Mix'], 'Hip-Hop/Rap', ['Alternative Rap', 'Dirty South', 'East Coast Rap', 'Freestyle', 'Gangsta Rap', 'Old School', 'Turntablism', 'Underground Hip-Hop', 'West Coast Rap'], 'Inspirational', ['Christian', 'Christian Metal', 'Christian Rap', 'Christian Rock', 'Classic Christian', 'Contemporary Gospel', 'Gospel', 'Praise/Worship', 'Sermons/Services', 'Southern Gospel', 'Traditional Gospel'], 'International', ['African', 'Arabic', 'Asian', 'Brazilian', 'Caribbean', 'Celtic', 'European', 'Filipino', 'Greek', 'Hawaiian/Pacific', 'Hindi', 'Indian', 'Japanese', 'Jewish', 'Mediterranean', 'Middle Eastern', 'North American', 'Soca', 'South American', 'Tamil', 'Worldbeat', 'Zouk'], 'Jazz', ['Acid Jazz', 'Avant Garde', 'Big Band', 'Bop', 'Classic Jazz', 'Cool Jazz', 'Fusion', 'Hard Bop', 'Latin Jazz', 'Smooth Jazz', 'Swing', 'Vocal Jazz', 'World Fusion'], 'Latin', ['Bachata', 'Banda', 'Bossa Nova', 'Cumbia', 'Latin Dance', 'Latin Pop', 'Latin Rap/Hip-Hop', 'Latin Rock', 'Mariachi', 'Merengue', 'Ranchera', 'Salsa', 'Tango', 'Tejano', 'Tropicalia'], 'Metal', ['Extreme Metal', 'Heavy Metal', 'Industrial Metal', 'Pop Metal/Hair', 'Rap Metal'], 'New Age', ['Environmental', 'Ethnic Fusion', 'Healing', 'Meditation', 'Spiritual'], 'Oldies', ['30s', '40s', '50s', '60s', '70s', '80s', '90s'], 'Pop', ['Adult Contemporary', 'Barbershop', 'Bubblegum Pop', 'Dance Pop', 'JPOP', 'Soft Rock', 'Teen Pop', 'Top 40', 'World Pop'], 'R&B/Urban', ['Classic R&B', 'Contemporary R&B', 'Doo Wop', 'Funk', 'Motown', 'Neo-Soul', 'Quiet Storm', 'Soul', 'Urban Contemporary'], 'Reggae', ['Contemporary Reggae', 'Dancehall', 'Dub', 'Pop-Reggae', 'Ragga', 'Reggaeton', 'Rock Steady', 'Roots Reggae', 'Ska'], 'Rock', ['Adult Album Alternative', 'British Invasion', 'Classic Rock', 'Garage Rock', 'Glam', 'Hard Rock', 'Jam Bands', 'Prog/Art Rock', 'Psychedelic', 'Rock & Roll', 'Rockabilly', 'Singer/Songwriter', 'Surf'], 'Seasonal/Holiday', ['Anniversary', 'Birthday', 'Christmas', 'Halloween', 'Hanukkah', 'Honeymoon', 'Valentine', 'Wedding'], 'Soundtracks', ['Anime', "Children's/Family", 'Original Score', 'Showtunes'], 'Talk', ['Comedy', 'Community', 'Educational', 'Government', 'News', 'Old Time Radio', 'Other Talk', 'Political', 'Scanner', 'Spoken Word', 'Sports']]
current = ""
default = "Pop"
empty = None
# redefine
streams = {}
def __init__(self, parent=None):
# override datamap fields //@todo: might need a cleaner method, also: do we really want the stream data in channels to be different/incompatible?
self.datamap = copy.deepcopy(self.datamap)
self.datamap[5][0] = "Rating"
self.datamap[5][2][0] = "rating"
self.datamap[3][0] = "Description"
self.datamap[3][2][0] = "description"
# superclass
ChannelPlugin.__init__(self, parent)
# read category thread from /listen/browse.live
def update_categories(self):
self.categories = []
# fetch page
html = http.get("http://www.live365.com/index.live", feedback=self.parent.status);
rx_genre = re.compile("""
href='/genres/([\w\d%+]+)'[^>]*>
( (?:<nobr>)? )
( \w[-\w\ /'.&]+ )
( (?:</a>)? )
""", re.X|re.S)
# collect
last = []
for uu in rx_genre.findall(html):
(link, sub, title, main) = uu
# main
if main and not sub:
self.categories.append(title)
self.categories.append(last)
last = []
# subcat
else:
last.append(title)
# don't forget last entries
self.categories.append(last)
# extract stream infos
def update_streams(self, cat, search=""):
# search / url
if (not search):
url = "http://www.live365.com/cgi-bin/directory.cgi?genre=" + self.cat2tag(cat) + "&rows=200" #+"&first=1"
else:
url = "http://www.live365.com/cgi-bin/directory.cgi?site=..&searchdesc=" + urllib.quote(search) + "&searchgenre=" + self.cat2tag(cat) + "&x=0&y=0"
html = http.get(url, feedback=self.parent.status)
# we only need to download one page, because live365 always only gives 200 results
# terse format
rx = re.compile(r"""
['"]Launch\((\d+).*?
['"](OK|PM_ONLY|SUBSCRIPTION).*?
href=['"](http://www.live365.com/stations/\w+)['"].*?
page['"]>([^<>]*)</a>.*?
CLASS="genre"[^>]*>(.+?)</TD>.+?
=["']audioQuality.+?>\w+\s+(\d+)\w<.+?
>DrawListenerStars\((\d+),.+?
>DrawRatingStars\((\d+),\s+(\d+),.*?
["']station_id=(\d+).+?
class=["']?desc-link[^>]+>([^<>]*)<
""", re.X|re.I|re.S|re.M)
# src="(http://www.live365.com/.+?/stationlogo\w+.jpg)".+?
# append entries to result list
__print__( html )
ls = []
for row in rx.findall(html):
__print__( row )
points = int(row[7])
count = int(row[8])
ls.append({
"launch_id": row[0],
"sofo": row[1], # subscribe-or-fuck-off status flags
"state": ("" if row[1]=="OK" else gtk.STOCK_STOP),
"homepage": entity_decode(row[2]),
"title": entity_decode(row[3]),
"genre": self.strip_tags(row[4]),
"bitrate": int(row[5]),
"listeners": int(row[6]),
"max": 0,
"rating": (points + count**0.4) / (count - 0.001*(count-0.1)), # prevents division by null, and slightly weights (more votes are higher scored than single votes)
"rating_points": points,
"rating_count": count,
# id for URL:
"station_id": row[9],
"url": self.base_url + "play/" + row[9],
"description": entity_decode(row[10]),
#"playing": row[10],
# "deleted": row[0] != "OK",
})
return ls
# faster if we do it in _update() prematurely
#def prepare(self, ls):
# GenericChannel.prepare(ls)
# for row in ls:
# if (not row["state"]):
# row["state"] = (gtk.STOCK_STOP, "") [row["sofo"]=="OK"]
# return ls
# html helpers
def cat2tag(self, cat):
return urllib.quote(cat.lower()) #re.sub("[^a-z]", "",
def strip_tags(self, s):
return re.sub("<.+?>", "", s)
|
Added channels/modarchive.py version [ac3a95273a].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# api: streamtuner2
# title: modarchive browser
#
#
# Just a genre browser.
#
# MOD files dodn't work with all audio players. And with the default
# download method, it'll receive a .zip archive with embeded .mod file.
# VLC in */* seems to work fine however.
#
import re
import http
from config import conf
from channels import *
from channels import __print__
from xml.sax.saxutils import unescape
# MODs
class modarchive (ChannelPlugin):
# description
title = "modarchive"
module = "modarchive"
homepage = "http://www.modarchive.org/"
version = 0.1
base = "http://modarchive.org/"
# keeps category titles->urls
catmap = {}
categories = []
# refresh category list
def update_categories(self):
html = http.get("http://modarchive.org/index.php?request=view_genres")
rx_current = re.compile(r"""
>\s+(\w[^<>]+)\s+</h1> |
<a\s[^>]+query=(\d+)&[^>]+>(\w[^<]+)</a>
""", re.S|re.X)
#-- archived shows
sub = []
self.categories = []
for uu in rx_current.findall(html):
(main, id, subname) = uu
if main:
if sub:
self.categories.append(sub)
sub = []
self.categories.append(main)
else:
sub.append(subname)
self.catmap[subname] = id
#
#-- 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)
# 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
# download links from dmoz listing
def update_streams(self, cat, force=0):
url = "http://modarchive.org/index.php?query="+self.catmap[cat]+"&request=search&search_type=genre"
html = http.get(url)
entries = []
rx_mod = re.compile("""
href="(http://modarchive.org/data/downloads.php[?]moduleid=(\d+)[#][^"]+)"
.*? /formats/(\w+).png"
.*? title="([^">]+)">([^<>]+)</a>
.*? >Rated</a>\s*(\d+)
""", re.X|re.S)
for uu in rx_mod.findall(html):
(url, id, fmt, title, file, rating) = uu
__print__( uu )
entries.append({
"genre": cat,
"url": url,
"id": id,
"format": self.mime_fmt(fmt) + "+zip",
"title": title,
"playing": file,
"listeners": int(rating),
"homepage": "http://modarchive.org/index.php?request=view_by_moduleid&query="+id,
})
# done
return entries
|
Added channels/musicgoal.py version [1d593a3fc6].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
#
# api: streamtuner2
# title: MUSICGOAL channel
# description: musicgoal.com/.de combines radio and podcast listings
# version: 0.1
# status: experimental
# pre-config: <const name="api"/>
#
# Musicgoal.com is a radio and podcast directory. This plugin tries to use
# the new API for accessing listing data.
#
#
# st2 modules
from config import conf
from mygtk import mygtk
import http
from channels import *
# python modules
import re
import json
# I wonder what that is for ---------------------------------------
class musicgoal (ChannelPlugin):
# desc
module = "musicgoal"
title = "MUSICGOAL"
version = 0.1
homepage = "http://www.musicgoal.com/"
base_url = homepage
listformat = "url/direct"
# settings
config = [
]
api_podcast = "http://www.musicgoal.com/api/?todo=export&todo2=%s&cat=%s&format=json&id=1000259223&user=streamtuner&pass=tralilala"
api_radio = "http://www.musicgoal.com/api/?todo=playlist&format=json&genre=%s&id=1000259223&user=streamtuner&pass=tralilala"
# categories are hardcoded
podcast = ["Arts", "Management", "Recreation", "Knowledge", "Nutrition", "Books", "Movies & TV", "Music", "News", "Business", "Poetry", "Politic", "Radio", "Science", "Science Fiction", "Religion", "Sport", "Technic", "Travel", "Health", "New"]
radio = ["Top radios", "Newcomer", "Alternative", "House", "Jazz", "Classic", "Metal", "Oldies", "Pop", "Rock", "Techno", "Country", "Funk", "Hip hop", "R&B", "Reggae", "Soul", "Indian", "Top40", "60s", "70s", "80s", "90s", "Sport", "Various", "Radio", "Party", "Christmas", "Firewall", "Auto DJ", "Audio-aacp", "Audio-ogg", "Video", "MyTop", "New", "World", "Full"]
categories = ["podcasts/", podcast, "radios/", radio]
#catmap = {"podcast": dict((i+1,v) for enumerate(self.podcast)), "radio": dict((i+1,v) for enumerate(self.radio))}
# nop
def update_categories(self):
pass
# request json API
def update_streams(self, cat, search=""):
# category type: podcast or radio
if cat in self.podcast:
grp = "podcast"
url = self.api_podcast % (grp, self.podcast.index(cat)+1)
elif cat in self.radio:
grp = "radio"
url = self.api_radio % cat.lower().replace(" ","").replace("&","")
else:
return []
# retrieve API data
data = http.ajax(url, None)
data = json.loads(data)
# tranform datasets
if grp == "podcast":
return [{
"genre": cat,
"title": row["titel"],
"homepage": row["url"],
"playing": str(row["typ"]),
#"id": row["id"],
#"listeners": int(row["2"]),
#"listformat": "text/html",
"url": "",
} for row in data]
else:
return [{
"format": self.mime_fmt(row["ctype"]),
"genre": row["genre"] or cat,
"url": "http://%s:%s/%s" % (row["host"], row["port"], row["pfad"]),
"listformat": "url/direct",
"id": row["id"],
"title": row["name"],
"playing": row["song"],
"homepage": row.get("homepage") or row.get("url"),
} for row in data]
|
Added channels/myoggradio.py version [37210dae1e].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
#
# api: streamtuner2
# title: MyOggRadio channel plugin
# description: open source internet radio directory MyOggRadio
# version: 0.5
# config:
# <var name="myoggradio_login" type="text" value="user:password" description="login account for myoggradio service" />
# priority: standard
# category: channel
# depends: json, StringIO
#
# MyOggRadio is an open source radio station directory. Because this matches
# well with streamtuner2, there's now a project partnership. Shared streams can easily
# be downloaded in this channel plugin. And streamtuner2 users can easily share their
# favourite stations into the MyOggRadio directory.
#
# Beforehand an account needs to be configured in the settings. (Registration
# on myoggradio doesn't require an email address or personal information.)
#
from channels import *
from config import conf
from action import action
import re
import json
from StringIO import StringIO
import copy
# open source radio sharing stie
class myoggradio(ChannelPlugin):
# description
title = "MyOggRadio"
module = "myoggradio"
homepage = "http://www.myoggradio.org/"
api = "http://ehm.homelinux.org/MyOggRadio/"
version = 0.5
listformat = "url/direct"
# config data
config = [
{"name":"myoggradio_login", "type":"text", "value":"user:password", "description":"Account for storing personal favourites."},
{"name":"myoggradio_morph", "type":"boolean", "value":0, "description":"Convert pls/m3u into direct shoutcast url."},
]
# hide unused columns
titles = dict(playing=False, listeners=False, bitrate=False)
# category map
categories = ['common', 'personal']
default = 'common'
current = 'common'
# prepare GUI
def __init__(self, parent):
ChannelPlugin.__init__(self, parent)
if parent:
mygtk.add_menu(parent.extensions, "Share in MyOggRadio", self.share)
# this is simple, there are no categories
def update_categories(self):
pass
# download links from dmoz listing
def update_streams(self, cat, force=0):
# result list
entries = []
# common
if (cat == "common"):
# fetch
data = http.get(self.api + "common.json")
entries = json.load(StringIO(data))
# bookmarks
elif (cat == "personal") and self.user_pw():
data = http.get(self.api + "favoriten.json?user=" + self.user_pw()[0])
entries = json.load(StringIO(data))
# unknown
else:
self.parent.status("Unknown category")
pass
# augment result list
for i,e in enumerate(entries):
entries[i]["homepage"] = self.api + "c_common_details.jsp?url=" + e["url"]
entries[i]["genre"] = cat
# send back
return entries
# upload a single station entry to MyOggRadio
def share(self, *w):
# get data
row = self.parent.row()
if row:
row = copy.copy(row)
# convert PLS/M3U link to direct ICY stream url
if conf.myoggradio_morph and self.parent.channel().listformat != "url/direct":
row["url"] = http.fix_url(action.srv(row["url"]))
# prevent double check-ins
if row["title"] in (r.get("title") for r in self.streams["common"]):
pass
elif row["url"] in (r.get("url") for r in self.streams["common"]):
pass
# send
else:
self.parent.status("Sharing station URL...")
self.upload(row)
sleep(0.5) # artificial slowdown, else user will assume it didn't work
# tell Gtk we've handled the situation
self.parent.status("Shared '" + row["title"][:30] + "' on MyOggRadio.org")
return True
# upload bookmarks
def send_bookmarks(self, entries=[]):
for e in (entries if entries else parent.bookmarks.streams["favourite"]):
self.upload(e)
# send row to MyOggRadio
def upload(self, e, form=0):
if e:
login = self.user_pw()
submit = {
"user": login[0], # api
"passwort": login[1], # api
"url": e["url"],
"bemerkung": e["title"],
"genre": e["genre"],
"typ": e["format"][6:],
"eintragen": "eintragen", # form
}
# just push data in, like the form does
if form:
self.login()
http.ajax(self.api + "c_neu.jsp", submit)
# use JSON interface
else:
http.ajax(self.api + "commonadd.json?" + urllib.urlencode(submit))
# authenticate against MyOggRadio
def login(self):
login = self.user_pw()
if login:
data = dict(zip(["benutzer", "passwort"], login))
http.ajax(self.api + "c_login.jsp", data)
# let's hope the JSESSIONID cookie is kept
# returns login (user,pw)
def user_pw(self):
if conf.myoggradio_login != "user:password":
return conf.myoggradio_login.split(":")
else: pass
|
Added channels/punkcast.py version [ba986159c3].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# api: streamtuner2
# title: punkcast listing
#
#
# Disables itself per default.
# ST1 looked prettier with random images within.
#
import re
import http
from config import conf
import action
from channels import *
from channels import __print__
# disable plugin per default
if "punkcast" not in vars(conf):
conf.plugins["punkcast"] = 0
# basic.ch broadcast archive
class punkcast (ChannelPlugin):
# description
title = "punkcast"
module = "punkcast"
homepage = "http://www.punkcast.com/"
version = 0.1
# keeps category titles->urls
catmap = {}
categories = ["list"]
default = "list"
current = "list"
# don't do anything
def update_categories(self):
pass
# get list
def update_streams(self, cat, force=0):
rx_link = re.compile("""
<a\shref="(http://punkcast.com/(\d+)/index.html)">
\s+<img[^>]+ALT="([^<">]+)"
""", re.S|re.X)
entries = []
#-- all from frontpage
for uu in rx_link.findall(http.get(self.homepage)):
(homepage, id, title) = uu
entries.append({
"genre": "?",
"title": title,
"playing": "PUNKCAST #"+id,
"format": "audio/mp3",
"homepage": homepage,
})
# done
return entries
# special handler for play
def play(self, row):
rx_sound = re.compile("""(http://[^"<>]+[.](mp3|ogg|m3u|pls|ram))""")
html = http.get(row["homepage"])
# look up ANY audio url
for uu in rx_sound.findall(html):
__print__( uu )
(url, fmt) = uu
action.action.play(url, self.mime_fmt(fmt), "url/direct")
return
# or just open webpage
action.action.browser(row["homepage"])
|
Added channels/shoutcast.py version [8bd878f25d].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
#
# api: streamtuner2
# title: shoutcast
# description: Channel/tab for Shoutcast.com directory
# depends: pq, re, http
# version: 1.2
# author: Mario
# original: Jean-Yves Lefort
#
# Shoutcast is a server software for audio streaming. It automatically spools
# station information on shoutcast.com, which this plugin can read out. But
# since the website format is often changing, we now use PyQuery HTML parsing
# in favour of regular expression (which still work, are faster, but not as
# reliable).
#
# This was previously a built-in channel plugin. It just recently was converted
# from a glade predefined GenericChannel into a ChannelPlugin.
#
#
# NOTES
#
# Just found out what Tunapie uses:
# http://www.shoutcast.com/sbin/newxml.phtml?genre=Top500
# It's a simpler list format, no need to parse HTML. However, it also lacks
# homepage links. But maybe useful as alternate fallback...
# Also:
# http://www.shoutcast.com/sbin/newtvlister.phtml?alltv=1
# http://www.shoutcast.com/sbin/newxml.phtml?search=
#
#
#
import http
import urllib
import re
from pq import pq
from config import conf
#from channels import * # works everywhere but in this plugin(???!)
import channels
__print__ = channels.__print__
# SHOUTcast data module ----------------------------------------
class shoutcast(channels.ChannelPlugin):
# desc
api = "streamtuner2"
module = "shoutcast"
title = "SHOUTcast"
version = 1.2
homepage = "http://www.shoutcast.com/"
base_url = "http://shoutcast.com/"
listformat = "audio/x-scpls"
# settings
config = [
dict(name="pyquery", type="boolean", value=0, description="Use more reliable PyQuery HTML parsing\ninstead of faster regular expressions."),
dict(name="debug", type="boolean", value=0, description="enable debug output"),
]
# categories
categories = ['Alternative', ['Adult Alternative', 'Britpop', 'Classic Alternative', 'College', 'Dancepunk', 'Dream Pop', 'Emo', 'Goth', 'Grunge', 'Hardcore', 'Indie Pop', 'Indie Rock', 'Industrial', 'Modern Rock', 'New Wave', 'Noise Pop', 'Power Pop', 'Punk', 'Ska', 'Xtreme'], 'Blues', ['Acoustic Blues', 'Chicago Blues', 'Contemporary Blues', 'Country Blues', 'Delta Blues', 'Electric Blues'], 'Classical', ['Baroque', 'Chamber', 'Choral', 'Classical Period', 'Early Classical', 'Impressionist', 'Modern', 'Opera', 'Piano', 'Romantic', 'Symphony'], 'Country', ['Americana', 'Bluegrass', 'Classic Country', 'Contemporary Bluegrass', 'Contemporary Country', 'Honky Tonk', 'Hot Country Hits', 'Western'], 'Decades', ['30s', '40s', '50s', '60s', '70s', '80s', '90s'], 'Easy Listening', ['Exotica', 'Light Rock', 'Lounge', 'Orchestral Pop', 'Polka', 'Space Age Pop'], 'Electronic', ['Acid House', 'Ambient', 'Big Beat', 'Breakbeat', 'Dance', 'Demo', 'Disco', 'Downtempo', 'Drum and Bass', 'Electro', 'Garage', 'Hard House', 'House', 'IDM', 'Jungle', 'Progressive', 'Techno', 'Trance', 'Tribal', 'Trip Hop'], 'Folk', ['Alternative Folk', 'Contemporary Folk', 'Folk Rock', 'New Acoustic', 'Traditional Folk', 'World Folk'], 'Inspirational', ['Christian', 'Christian Metal', 'Christian Rap', 'Christian Rock', 'Classic Christian', 'Contemporary Gospel', 'Gospel', 'Southern Gospel', 'Traditional Gospel'], 'International', ['African', 'Arabic', 'Asian', 'Bollywood', 'Brazilian', 'Caribbean', 'Celtic', 'Chinese', 'European', 'Filipino', 'French', 'Greek', 'Hindi', 'Indian', 'Japanese', 'Jewish', 'Klezmer', 'Korean', 'Mediterranean', 'Middle Eastern', 'North American', 'Russian', 'Soca', 'South American', 'Tamil', 'Worldbeat', 'Zouk'], 'Jazz', ['Acid Jazz', 'Avant Garde', 'Big Band', 'Bop', 'Classic Jazz', 'Cool Jazz', 'Fusion', 'Hard Bop', 'Latin Jazz', 'Smooth Jazz', 'Swing', 'Vocal Jazz', 'World Fusion'], 'Latin', ['Bachata', 'Banda', 'Bossa Nova', 'Cumbia', 'Latin Dance', 'Latin Pop', 'Latin Rock', 'Mariachi', 'Merengue', 'Ranchera', 'Reggaeton', 'Regional Mexican', 'Salsa', 'Tango', 'Tejano', 'Tropicalia'], 'Metal', ['Black Metal', 'Classic Metal', 'Extreme Metal', 'Grindcore', 'Hair Metal', 'Heavy Metal', 'Metalcore', 'Power Metal', 'Progressive Metal', 'Rap Metal'], 'Misc', [], 'New Age', ['Environmental', 'Ethnic Fusion', 'Healing', 'Meditation', 'Spiritual'], 'Pop', ['Adult Contemporary', 'Barbershop', 'Bubblegum Pop', 'Dance Pop', 'Idols', 'JPOP', 'Oldies', 'Soft Rock', 'Teen Pop', 'Top 40', 'World Pop'], 'Public Radio', ['College', 'News', 'Sports', 'Talk'], 'Rap', ['Alternative Rap', 'Dirty South', 'East Coast Rap', 'Freestyle', 'Gangsta Rap', 'Hip Hop', 'Mixtapes', 'Old School', 'Turntablism', 'West Coast Rap'], 'Reggae', ['Contemporary Reggae', 'Dancehall', 'Dub', 'Ragga', 'Reggae Roots', 'Rock Steady'], 'Rock', ['Adult Album Alternative', 'British Invasion', 'Classic Rock', 'Garage Rock', 'Glam', 'Hard Rock', 'Jam Bands', 'Piano Rock', 'Prog Rock', 'Psychedelic', 'Rockabilly', 'Surf'], 'Soundtracks', ['Anime', 'Kids', 'Original Score', 'Showtunes', 'Video Game Music'], 'Talk', ['BlogTalk', 'Comedy', 'Community', 'Educational', 'Government', 'News', 'Old Time Radio', 'Other Talk', 'Political', 'Scanner', 'Spoken Word', 'Sports', 'Technology'], 'Themes', ['Adult', 'Best Of', 'Chill', 'Eclectic', 'Experimental', 'Female', 'Heartache', 'Instrumental', 'LGBT', 'Party Mix', 'Patriotic', 'Rainy Day Mix', 'Reality', 'Sexy', 'Shuffle', 'Travel Mix', 'Tribute', 'Trippy', 'Work Mix']]
#["default", [], 'TopTen', [], 'Alternative', ['College', 'Emo', 'Hardcore', 'Industrial', 'Punk', 'Ska'], 'Americana', ['Bluegrass', 'Blues', 'Cajun', 'Folk'], 'Classical', ['Contemporary', 'Opera', 'Symphonic'], 'Country', ['Bluegrass', 'New Country', 'Western Swing'], 'Electronic', ['Acid Jazz', 'Ambient', 'Breakbeat', 'Downtempo', 'Drum and Bass', 'House', 'Trance', 'Techno'], 'Hip Hop', ['Alternative', 'Hardcore', 'New School', 'Old School', 'Turntablism'], 'Jazz', ['Acid Jazz', 'Big Band', 'Classic', 'Latin', 'Smooth', 'Swing'], 'Pop/Rock', ['70s', '80s', 'Classic', 'Metal', 'Oldies', 'Pop', 'Rock', 'Top 40'], 'R&B/Soul', ['Classic', 'Contemporary', 'Funk', 'Smooth', 'Urban'], 'Spiritual', ['Alternative', 'Country', 'Gospel', 'Pop', 'Rock'], 'Spoken', ['Comedy', 'Spoken Word', 'Talk'], 'World', ['African', 'Asian', 'European', 'Latin', 'Middle Eastern', 'Reggae'], 'Other/Mixed', ['Eclectic', 'Film', 'Instrumental']]
current = ""
default = "Alternative"
empty = ""
# redefine
streams = {}
# extracts the category list from shoutcast.com,
# sub-categories are queried per 'AJAX'
def update_categories(self):
html = http.get(self.base_url)
self.categories = ["default"]
__print__( html )
# <h2>Radio Genres</h2>
rx_main = re.compile(r'<li class="prigen" id="(\d+)".+?<a href="/radio/([\w\s]+)">[\w\s]+</a></li>', re.S)
rx_sub = re.compile(r'<a href="/radio/([\w\s\d]+)">[\w\s\d]+</a></li>')
for uu in rx_main.findall(html):
__print__(uu)
(id,name) = uu
name = urllib.unquote(name)
# main category
self.categories.append(name)
# sub entries
html = http.ajax("http://shoutcast.com/genre.jsp", {"genre":name, "id":id})
__print__(html)
sub = rx_sub.findall(html)
self.categories.append(sub)
# it's done
__print__(self.categories)
conf.save("cache/categories_shoutcast", self.categories)
pass
#def strip_tags(self, s):
# rx = re.compile(""">(\w+)<""")
# return " ".join(rx.findall(s))
# downloads stream list from shoutcast for given category
def update_streams(self, cat, search=""):
if (not cat or cat == self.empty):
__print__("nocat")
return []
ucat = urllib.quote(cat)
# new extraction regex
if not conf.get("pyquery") or not pq:
rx_stream = re.compile("""
<a\s+class="?playbutton\d?[^>]+id="(\d+)".+?
<a\s+class="[\w\s]*title[\w\s]*"[^>]+href="(http://[^">]+)"[^>]*>([^<>]+)</a>.+?
(?:Recently\s*played|Coming\s*soon|Now\s*playing):\s*([^<]*).+?
ners">(\d*)<.+?
bitrate">(\d*)<.+?
type">([MP3AAC]*)
""", re.S|re.I|re.X)
rx_next = re.compile("""onclick="showMoreGenre""")
# loop
entries = []
next = 0
max = int(conf.max_streams)
count = max
while (next < max):
# page
url = "http://www.shoutcast.com/genre-ajax/" + ucat
referer = url.replace("/genre-ajax", "/radio")
params = { "strIndex":"0", "count":str(count), "ajax":"true", "mode":"listeners", "order":"desc" }
html = http.ajax(url, params, referer) #,feedback=self.parent.status)
__print__(html)
# regular expressions
if not conf.get("pyquery") or not pq:
# extract entries
self.parent.status("parsing document...")
__print__("loop-rx")
for uu in rx_stream.findall(html):
(id, homepage, title, playing, ls, bit, fmt) = uu
__print__(uu)
entries += [{
"title": self.entity_decode(title),
"url": "http://yp.shoutcast.com/sbin/tunein-station.pls?id=" + id,
"homepage": http.fix_url(homepage),
"playing": self.entity_decode(playing),
"genre": cat, #self.strip_tags(uu[4]),
"listeners": int(ls),
"max": 0, #int(uu[6]),
"bitrate": int(bit),
"format": self.mime_fmt(fmt),
}]
# PyQuery parsing
else:
# iterate over DOM
for div in (pq(e) for e in pq(html).find("div.dirlist")):
entries.append({
"title": div.find("a.clickabletitleGenre, div.stationcol a").attr("title"),
"url": div.find("a.playbutton, a.playbutton1, a.playimage").attr("href"),
"homepage": http.fix_url(div.find("a.playbutton.clickabletitle, a[target=_blank], a.clickabletitleGenre, a.clickabletitle, div.stationcol a, a").attr("href")),
"playing": div.find("div.playingtextGenre, div.playingtext").attr("title"),
"listeners": int(div.find("div.dirlistners").text()),
"bitrate": int(div.find("div.dirbitrate").text()),
"format": self.mime_fmt(div.find("div.dirtype").text()),
"max": 0,
"genre": cat,
# "title2": e.find("a.playbutton").attr("name"),
})
# display partial results (not strictly needed anymore, because we fetch just one page)
self.parent.status()
self.update_streams_partially_done(entries)
# more pages to load?
if (re.search(rx_next, html)):
next += count
else:
next = 99999
#fin
__print__(entries)
return entries
|
Added channels/timer.py version [c3eb5a36c2].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
#
# api: streamtuner2
# title: radio scheduler
# description: time play/record events for radio stations
# depends: kronos
# version: 0.5
# config:
# category: features
# priority: optional
# support: unsupported
#
# Okay, while programming this, I missed the broadcast I wanted to hear. Again(!)
# But still this is a useful extension, as it allows recording and playing specific
# stations at a programmed time and interval. It accepts a natural language time
# string when registering a stream. (Via streams menu > extension > add timer)
#
# Programmed events are visible in "timer" under the "bookmarks" channel. Times
# are stored in the description field, and can thus be edited. However, after editing
# times manuall, streamtuner2 must be restarted for the changes to take effect.
#
from channels import *
import kronos
from mygtk import mygtk
from action import action
import copy
# timed events (play/record) within bookmarks tab
class timer:
# plugin info
module = "timer"
title = "Timer"
version = 0.5
# configuration settings
config = [
]
timefield = "playing"
# kronos scheduler list
sched = None
# prepare gui
def __init__(self, parent):
if parent:
# keep reference to main window
self.parent = parent
self.bookmarks = parent.bookmarks
# add menu
mygtk.add_menu(self.parent.extensions, "Add timer for station", self.edit_timer)
# target channel
if not self.bookmarks.streams.get("timer"):
self.bookmarks.streams["timer"] = [{"title":"--- timer events ---"}]
self.bookmarks.add_category("timer")
self.streams = self.bookmarks.streams["timer"]
# widgets
parent.signal_autoconnect({
"timer_ok": self.add_timer,
"timer_cancel": lambda w,*a: self.parent.timer_dialog.hide() or 1,
})
# prepare spool
self.sched = kronos.ThreadedScheduler()
for row in self.streams:
try: self.queue(row)
except Exception,e: print("queuing error", e)
self.sched.start()
# display GUI for setting timespec
def edit_timer(self, *w):
self.parent.timer_dialog.show()
self.parent.timer_value.set_text("Fri,Sat 20:00-21:00 play")
# close dialog,get data
def add_timer(self, *w):
self.parent.timer_dialog.hide()
row = self.parent.row()
row = copy.copy(row)
# add data
row["listformat"] = "url/direct" #self.parent.channel().listformat
if row.get(self.timefield):
row["title"] = row["title"] + " -- " + row[self.timefield]
row[self.timefield] = self.parent.timer_value.get_text()
# store
self.save_timer(row)
# store row in timer database
def save_timer(self, row):
self.streams.append(row)
self.bookmarks.save()
self.queue(row)
pass
# add event to list
def queue(self, row):
# chk
if not row.get(self.timefield) or not row.get("url"):
#print("NO TIME DATA", row)
return
# extract timing parameters
_ = row[self.timefield]
days = self.days(_)
time = self.time(_)
duration = self.duration(_)
# which action
if row[self.timefield].find("rec")>=0:
activity, action_method = "record", self.record
else:
activity, action_method = "play", self.play
# add
task = self.sched.add_daytime_task(action_method, activity, days, None, time, kronos.method.threaded, [row], {})
#__print__( "queue", act, self.sched, (action_method, act, days, None, time, kronos.method.threaded, [row], {}), task.get_schedule_time(True) )
# converts Mon,Tue,... into numberics 1-7
def days(self, s):
weekdays = ["su", "mo", "tu", "we", "th", "fr", "sa", "su"]
r = []
for day in re.findall("\w\w+", s.lower()):
day = day[0:2]
if day in weekdays:
r.append(weekdays.index(day))
return list(set(r))
# get start time 18:00
def time(self, s):
r = re.search("(\d+):(\d+)", s)
return int(r.group(1)), int(r.group(2))
# convert "18:00-19:15" to minutes
def duration(self, s):
try:
r = re.search("(\d+:\d+)\s*(\.\.+|-+)\s*(\d+:\d+)", s)
start = self.time(r.group(1))
end = self.time(r.group(3))
duration = (end[0] - start[0]) * 60 + (end[1] - start[1])
return int(duration) # in minutes
except:
return 0 # no limit
# action wrapper
def play(self, row, *args, **kwargs):
action.play(
url = row["url"],
audioformat = row.get("format","audio/mp3"),
listformat = row.get("listformat","url/direct"),
)
# action wrapper
def record(self, row, *args, **kwargs):
#print("TIMED RECORD")
# extra params
duration = self.duration(row.get(self.timefield))
if duration:
append = " -a %S.%d.%q -l "+str(duration*60) # make streamripper record a whole broadcast
else:
append = ""
# start recording
action.record(
url = row["url"],
audioformat = row.get("format","audio/mp3"),
listformat = row.get("listformat","url/direct"),
append = append,
)
def test(self, row, *args, **kwargs):
print("TEST KRONOS", row)
|
Added channels/tv.py version [e09d4b20ac].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
#
# 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 <station> 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.py version [8ad5a64170].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
#
# api: streamtuner2
# title: Xiph.org
# description: Xiph/ICEcast radio directory
# version: 0.1
#
#
# Xiph.org maintains the Ogg streaming standard and Vorbis audio compression
# format, amongst others. The ICEcast server is an alternative to SHOUTcast.
# But it turns out, that Xiph lists only MP3 streams, no OGG. And the directory
# is less encompassing than Shoutcast.
#
#
#
#
# streamtuner2 modules
from config import conf
from mygtk import mygtk
import http
from channels import *
from channels import __print__
# python modules
import re
from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities
import xml.dom.minidom
# I wonder what that is for ---------------------------------------
class xiph (ChannelPlugin):
# desc
api = "streamtuner2"
module = "xiph"
title = "Xiph.org"
version = 0.1
homepage = "http://dir.xiph.org/"
base_url = "http://dir.xiph.org/"
yp = "yp.xml"
listformat = "url/http"
config = [
{"name":"xiph_min_bitrate", "value":64, "type":"int", "description":"minimum bitrate, filter anything below", "category":"filter"}
]
# content
categories = ["all", [], ]
current = ""
default = "all"
empty = None
# prepare category names
def __init__(self, parent=None):
self.categories = ["all"]
self.filter = {}
for main in self.genres:
if (type(main) == str):
id = main.split("|")
self.categories.append(id[0].title())
self.filter[id[0]] = main
else:
l = []
for sub in main:
id = sub.split("|")
l.append(id[0].title())
self.filter[id[0]] = sub
self.categories.append(l)
# GUI
ChannelPlugin.__init__(self, parent)
# just counts genre tokens, does not automatically create a category tree from it
def update_categories(self):
g = {}
for row in self.streams["all"]:
for t in row["genre"].split():
if g.has_key(t):
g[t] += 1
else:
g[t] = 0
g = [ [v[1],v[0]] for v in g.items() ]
g.sort()
g.reverse()
for row in g:
pass
__print__( ' "' + row[1] + '", #' + str(row[0]) )
# xml dom node shortcut to text content
def x(self, entry, name):
e = entry.getElementsByTagName(name)
if (e):
if (e[0].childNodes):
return e[0].childNodes[0].data
# convert bitrate string to integer
# (also convert "Quality \d+" to pseudo bitrate)
def bitrate(self, s):
uu = re.findall("(\d+)", s)
if uu:
br = uu[0]
if br > 10:
return int(br)
else:
return int(br * 25.6)
else:
return 0
# downloads stream list from shoutcast for given category
def update_streams(self, cat, search=""):
# there is actually just a single category to download,
# all else are virtual
if (cat == "all"):
#-- get data
yp = http.get(self.base_url + self.yp, 1<<22, feedback=self.parent.status)
#-- extract
l = []
for entry in xml.dom.minidom.parseString(yp).getElementsByTagName("entry"):
bitrate = self.bitrate(self.x(entry, "bitrate"))
if conf.xiph_min_bitrate and bitrate and bitrate >= int(conf.xiph_min_bitrate):
l.append({
"title": str(self.x(entry, "server_name")),
"url": str(self.x(entry, "listen_url")),
"format": self.mime_fmt(str(self.x(entry, "server_type"))[6:]),
"bitrate": bitrate,
"channels": str(self.x(entry, "channels")),
"samplerate": str(self.x(entry, "samplerate")),
"genre": str(self.x(entry, "genre")),
"playing": str(self.x(entry, "current_song")),
"listeners": 0,
"max": 0, # this information is in the html view, but not in the yp.xml (seems pretty static, we might as well make it a built-in list)
"homepage": "",
})
# filter out a single subtree
else:
rx = re.compile(self.filter.get(cat.lower(), cat.lower()))
l = []
for i,row in enumerate(self.streams["all"]):
if rx.search(row["genre"]):
l.append(row)
# send back the list
return l
genres = [
"scanner", #442
"rock", #305
[
"metal|heavy", #36
],
"various", #286
[
"mixed", #96
],
"pop", #221
[
"top40|top|40|top 40", #32
"charts|hits", #20+4
"80s", #68
"90s", #20
"disco", #17
"remixes", #10
],
"electronic|electro", #33
[
"dance", #161
"house", #106
"trance", #82
"techno", #72
"chillout", #16
"lounge", #12
],
"alternative", #68
[
"college", #32
"student", #20
"progressive", #20
],
"classical|classic", #58+20
"live", #57
"jazz", #42
[
"blues", #19
],
"talk|spoken|speak", #41
[
"news", #39
"public", #12
"info", #5
],
"world|international", #25
[
"latin", #34
"reggae", #12
"indie", #12
"folk", #9
"schlager", #14
"jungle", #13
"country", #7
"russian", #6
],
"hip hop|hip|hop", #34
[
"oldschool", #10
"rap",
],
"ambient", #34
"adult", #33
## "music", #32
"oldies", #31
[
"60s", #2
"70s", #17
],
"religious", #4
[
"christian|bible", #14
],
"rnb|r&b", #12
[
"soul", #11
"funk", #24
"urban", #11
],
"other", #25
[
"deep", #14
"soft", #12
"minimal", #12
"eclectic", #12
"drum", #12
"bass", #12
"experimental", #11
"hard", #10
"funky", #10
"downtempo", #10
"slow", #9
"break", #9
"electronica", #8
"dub", #8
"retro", #7
"punk", #7
"psychedelic", #7
"goa", #7
"freeform", #7
"c64", #7
"breaks", #7
"anime", #7
"variety", #6
"psytrance", #6
"island", #6
"downbeat", #6
"underground", #5
"newage", #5
"gothic", #5
"dnb", #5
"club", #5
"acid", #5
"video", #4
"trip", #4
"pure", #4
"industrial", #4
"groove", #4
"gospel", #4
"gadanie", #4
"french", #4
"dark", #4
"chill", #4
"age", #4
"wave", #3
"vocal", #3
"tech", #3
"studio", #3
"relax", #3
"rave", #3
"hardcore", #3
"breakbeat", #3
"avantgarde", #3
"swing", #2
"soundtrack", #2
"salsa", #2
"italian", #2
"independant", #2
"groovy", #2
"european", #2
"darkwave", #2
],
]
|
Added cli.py version [a682821821].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
#
# api: streamtuner2
# title: CLI interface
# description: allows to call streamtuner2 from the commandline
# status: experimental
# version: 0.3
#
# Returns JSON data when queried. Usually returns cache data, but the
# "category" module always loads fresh info from the directory servers.
#
# Not all channel plugins are gtk-free yet. And some workarounds are
# used here to not upset channel plugins about a missing parent window.
#
#
#
import sys
#from channels import *
import http
import action
from config import conf
import json
# CLI
class StreamTunerCLI (object):
# plugin info
title = "CLI interface"
version = 0.3
# channel plugins
channel_modules = ["shoutcast", "xiph", "internet_radio_org_uk", "jamendo", "myoggradio", "live365"]
current_channel = "cli"
plugins = {} # only populated sparsely by .stream()
# start
def __init__(self):
# fake init
action.action.main = empty_parent()
action.action.main.current_channel = self.current_channel
# check if enough arguments, else help
if len(sys.argv)<3:
a = self.help
# first cmdline arg == action
else:
command = sys.argv[1]
a = self.__getattribute__(command)
# run
result = a(*sys.argv[2:])
if result:
self.json(result)
# show help
def help(self, *args):
print("""
syntax: streamtuner2 action [channel] "stream title"
from cache:
streamtuner2 stream shoutcast frequence
streamtuner2 dump xiph
streamtuner2 play "..."
streamtuner2 url "..."
load fresh:
streamtuner2 category shoutcast "Top 40"
streamtuner2 categories xiph
""")
# prints stream data from cache
def stream(self, *args):
# optional channel name, title
if len(args) > 1:
(channel_list, title) = args
channel_list = channel_list.split(",")
else:
title = list(args).pop()
channel_list = self.channel_modules
# walk through channel plugins, categories, rows
title = title.lower()
for channel in channel_list:
self.current_channel = channel
c = self.channel(channel)
self.plugins[channel] = c
c.cache()
for cat in c.streams:
for row in c.streams[cat]:
if row and row.get("title","").lower().find(title)>=0:
return(row)
# just get url
def url(self, *args):
row = self.stream(*args)
if row.get("url"):
print(row["url"])
# run player
def play(self, *args):
row = self.stream(*args)
if row.get("url"):
#action.action.play(row["url"], audioformat=row.get("format","audio/mp3"))
self.plugins[self.current_channel].play(row)
# return cache data 1:1
def dump(self, channel):
c = self.channel(channel)
c.cache()
return c.streams
# load from server
def category(self, module, cat):
c = self.channel(module)
r = c.update_streams(cat)
[c.postprocess(row) for row in r]
return r
# load from server
def categories(self, module):
c = self.channel(module)
c.cache()
r = c.update_categories()
if not r:
r = c.categories
if c.__dict__.get("empty"):
del r[0]
return r
# load module
def channel(self, module):
plugin = __import__("channels."+module, None, None, [""])
plugin_class = plugin.__dict__[module]
p = plugin_class(None)
p.parent = empty_parent()
return p
# load all channel modules
def channels(self, channels=None):
if channels:
channels = channels.split(",")
else:
channels = self.channel_modules
return (self.channel(module) for module in channels)
# pretty print json
def json(self, dat):
print(json.dumps(dat, sort_keys=True, indent=2))
# trap for some main window calls
class empty_parent (object):
channel = {}
null = lambda *a: None
status = null
thread = null
|
Added config.py version [791971aff7].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
#
# encoding: UTF-8
# api: streamtuner2
# type: class
# title: global config object
# description: reads ~/.config/streamtuner/*.json files
#
# In the main application or module files which need access
# to a global conf object, just import this module as follows:
#
# from config import conf
#
# Here conf is already an instantiation of the underlying
# Config class.
#
import os
import sys
import pson
import gzip
#-- 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"]
self.__dict__ = self # let's pray this won't leak memory due to recursion issues
# prepare
self.defaults()
self.xdg()
# runtime
dirs = ["/usr/share/streamtuner2", "/usr/local/share/streamtuner2", sys.path[0], "."]
self.share = [d for d in dirs if os.path.exists(d)][0]
# settings from last session
last = self.load("settings")
if (last):
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 = {
"audio/mp3": "audacious ", # %u for url to .pls, %g for downloaded .m3u
"audio/ogg": "audacious ",
"audio/aac": "amarok -l ",
"audio/x-pn-realaudio": "vlc ",
"audio/*": "totem ",
"*/*": "vlc %srv",
}
self.record = {
"*/*": "x-terminal-emulator -e streamripper %srv",
# x-terminal-emulator -e streamripper %srv -d /home/***USERNAME***/Musik
}
self.plugins = {
"bookmarks": 1, # built-in plugins, cannot be disabled
"shoutcast": 1,
"punkcast": 0, # disable per default
}
self.tmp = os.environ.get("TEMP", "/tmp")
self.max_streams = "120"
self.show_bookmarks = 1
self.show_favicons = 1
self.load_favicon = 1
self.heuristic_bookmark_update = 1
self.retain_deleted = 1
self.auto_save_appstate = 1
self.theme = "" #"MountainDew"
self.debug = False
self.channel_order = "shoutcast, xiph, internet_radio_org_uk, jamendo, myoggradio, .."
self.reuse_m3u = 1
self.google_homepage = 1
# each plugin has a .config dict list, we add defaults here
def add_plugin_defaults(self, config, module=""):
# options
for opt in config:
if ("name" in opt) and ("value" in opt) and (opt["name"] not in vars(self)):
self.__dict__[opt["name"]] = opt["value"]
# plugin state
if module and module not in conf.plugins:
conf.plugins[module] = 1
# http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
def xdg(self):
home = os.environ.get("HOME", self.tmp)
config = os.environ.get("XDG_CONFIG_HOME", home+"/.config")
# storage dir
self.dir = config + "/" + "streamtuner2"
# create if necessary
if (not os.path.exists(self.dir)):
os.makedirs(self.dir)
# 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):
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("/")]
subdir = self.dir + "/" + subdir
if (not os.path.exists(subdir)):
os.mkdir(subdir)
open(subdir+"/.nobackup", "w").close()
# write
file = self.dir + "/" + name
# .gz or normal file
if gz:
f = gzip.open(file+".gz", "w")
if os.path.exists(file):
os.unlink(file)
else:
f = open(file, "w")
# encode
pson.dump(data, f, indent=(4 if nice else None))
f.close()
# retrieve data from config file
def load(self, name):
name = name + ".json"
file = self.dir + "/" + name
try:
# .gz or normal file
if os.path.exists(file + ".gz"):
f = gzip.open(file + ".gz", "r")
elif os.path.exists(file):
f = open(file, "r")
else:
return # file not found
# decode
r = pson.load(f)
f.close()
return r
except (Exception), e:
print("PSON parsing error (in "+name+")", e)
# recursive dict update
def update(self, with_new_data):
for key,value in with_new_data.iteritems():
if type(value) == dict:
self[key].update(value)
else:
self[key] = value
# descends into sub-dicts instead of wiping them with subkeys
#-- actually fill global conf instance
conf = ConfigDict()
|
Added favicon.py version [f5d4a162a1].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 |
#
# encoding: utf-8
# api: python
# title: favicon download
# description: retrieves favicons for station homepages, plus utility code for display preparation
# config:
# <var name="always_google" value="1" description="always use google favicon to png conversion service" />
# <var name="only_google" value="1" description="don't try other favicon retrieval methods, if google service fails" />
# <var name="delete_google_stub" value="1" description="delete placeholder favicons" />
# type: module
#
#
# This module fetches favicon.ico files and prepares .png images for each domain
# in the stations list. Homepage URLs are used for this.
#
# Files end up in:
# /home/user/.config/streamtuner2/icons/www.example.org.png
#
# Currently relies on Google conversion service, because urllib+PIL conversion
# method is still flaky, and a bit slower. Future version might use imagemagick.
#
always_google = 1 # use favicon service for speed
only_google = 1 # if that fails, try our other/slower methods?
delete_google_stub = 1 # don't keep placeholder images
google_placeholder_filesizes = (726,896)
import os, os.path
import urllib
import re
import urlparse
from config import conf
try: from processing import Process as Thread
except: from threading import Thread
import http
# 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
tried_urls = []
# walk through entries
def download_all(entries):
t = Thread(target= download_thread, args= ([entries]))
t.start()
def download_thread(entries):
for e in entries:
# try just once
if e.get("homepage") in tried_urls:
pass
# retrieve specific img url as favicon
elif e.get("img"):
pass
# favicon from homepage URL
elif e.get("homepage"):
download(e["homepage"])
# remember
tried_urls.append(e.get("homepage"))
pass
# download a single favicon for currently playing station
def download_playing(row):
if conf.google_homepage and not row.get("homepage"):
google_find_homepage(row)
if conf.load_favicon and row.get("homepage"):
download_all([row])
pass
#--- unrelated ---
def google_find_homepage(row):
""" Searches for missing homepage URL via Google. """
if row.get("url") not in tried_urls:
tried_urls.append(row.get("url"))
rx_t = re.compile('^(([^-:]+.?){1,2})')
rx_u = re.compile('"(http://[^"]+)" class=l')
# extract first title parts
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)
# find first URL hit
url = rx_u.search(html)
if url:
row["homepage"] = http.fix_url(url.group(1))
pass
#-----------------
# extract domain name
def domain(url):
if url.startswith("http://"):
return url[7:url.find("/", 8)] # we assume our URLs are fixed already (http://example.org/ WITH trailing slash!)
else:
return "null"
# local filename
def name(url):
return domain(url) + ".png"
# local filename
def file(url):
icon_dir = conf.dir + "/icons"
if not os.path.exists(icon_dir):
os.mkdir(icon_dir)
open(icon_dir+"/.nobackup", "w").close()
return icon_dir + "/" + name(url)
# does the favicon exist
def available(url):
return os.path.exists(file(url))
# download favicon for given URL
def download(url):
# skip if .png for domain already exists
if available(url):
return
# fastest method, so default to google for now
if always_google:
google_ico2png(url)
if available(url) or only_google:
return
try: # look for /favicon.ico first
#print("favicon.ico")
direct_download("http://"+domain(url)+"/favicon.ico", file(url))
except:
try: # extract facicon filename from website <link rel>
#print("html <rel favicon>")
html_download(url)
except: # fallback
#print("google ico2png")
google_ico2png(url)
# retrieve PNG via Google ico2png
def google_ico2png(url):
#try:
GOOGLE = "http://www.google.com/s2/favicons?domain="
(fn, headers) = urllib.urlretrieve(GOOGLE+domain(url), file(url))
# test for stub image
if delete_google_stub and (filesize(fn) in google_placeholder_filesizes):
os.remove(fn)
def filesize(fn):
return os.stat(fn).st_size
# mime magic
def filetype(fn):
f = open(fn, "rb")
bin = f.read(4)
f.close()
if bin[1:3] == "PNG":
return "image/png"
else:
return "*/*"
# favicon.ico
def direct_download(favicon, fn):
# try:
# URL download
r = urllib.urlopen(favicon)
headers = r.info()
# abort on
if r.getcode() >= 300:
raise "HTTP error", r.getcode()
if not headers["Content-Type"].lower().find("image/"):
raise "can't use text/* content"
# save file
fn_tmp = fn+".tmp"
f = open(fn_tmp, "wb")
f.write(r.read(32768))
f.close()
# check type
if headers["Content-Type"].lower()=="image/png" and favicon.find(".png") and filetype(fn)=="image/png":
pngresize(fn_tmp)
os.mv(fn_tmp, fn)
else:
ico2png(fn_tmp, fn)
os.remove(fn_tmp)
# except:
# "File not found" and False
# peek at URL, download favicon.ico <link rel>
def html_download(url):
# <link rel>
#try:
# download html, look for @href in <link rel=shortcut icon>
r = urllib.urlopen(url)
html = r.read(4096)
r.close()
rx = re.compile("""<link[^<>]+rel\s*=\s*"?\s*(?:shortcut\s+|fav)?icon[^<>]+href=["'](?P<href>[^<>"']+)["'<>\s].""")
favicon = "".join(rx.findall(html))
# url or
if favicon.startswith("http://"):
None
# just /pathname
else:
favicon = urlparse.urljoin(url, favicon)
#favicon = "http://" + domain(url) + "/" + favicon
# download
direct_download(favicon, file(url))
#
# title: workaround for PIL.Image to preserve the transparency for .ico import
#
# http://stackoverflow.com/questions/987916/how-to-determine-the-transparent-color-index-of-ico-image-with-pil
# http://djangosnippets.org/snippets/1287/
#
# Author: dc
# Posted: January 17, 2009
# Languag: Python
# Django Version: 1.0
# Tags: pil image ico
# Score: 2 (after 2 ratings)
#
import operator
import struct
try:
from PIL import BmpImagePlugin, PngImagePlugin, Image
except Exception, e:
print("no PIL", e)
always_google = 1
only_google = 1
def load_icon(file, index=None):
'''
Load Windows ICO image.
See http://en.wikipedia.org/w/index.php?oldid=264332061 for file format
description.
'''
if isinstance(file, basestring):
file = open(file, 'rb')
try:
header = struct.unpack('<3H', file.read(6))
except:
raise IOError('Not an ICO file')
# Check magic
if header[:2] != (0, 1):
raise IOError('Not an ICO file')
# Collect icon directories
directories = []
for i in xrange(header[2]):
directory = list(struct.unpack('<4B2H2I', file.read(16)))
for j in xrange(3):
if not directory[j]:
directory[j] = 256
directories.append(directory)
if index is None:
# Select best icon
directory = max(directories, key=operator.itemgetter(slice(0, 3)))
else:
directory = directories[index]
# Seek to the bitmap data
file.seek(directory[7])
prefix = file.read(16)
file.seek(-16, 1)
if PngImagePlugin._accept(prefix):
# Windows Vista icon with PNG inside
image = PngImagePlugin.PngImageFile(file)
else:
# Load XOR bitmap
image = BmpImagePlugin.DibImageFile(file)
if image.mode == 'RGBA':
# Windows XP 32-bit color depth icon without AND bitmap
pass
else:
# Patch up the bitmap height
image.size = image.size[0], image.size[1] >> 1
d, e, o, a = image.tile[0]
image.tile[0] = d, (0, 0) + image.size, o, a
# Calculate AND bitmap dimensions. See
# http://en.wikipedia.org/w/index.php?oldid=264236948#Pixel_storage
# for description
offset = o + a[1] * image.size[1]
stride = ((image.size[0] + 31) >> 5) << 2
size = stride * image.size[1]
# Load AND bitmap
file.seek(offset)
string = file.read(size)
mask = Image.fromstring('1', image.size, string, 'raw',
('1;I', stride, -1))
image = image.convert('RGBA')
image.putalpha(mask)
return image
# convert .ico file to .png format
def ico2png(ico, png_fn):
#print("ico2png", ico, png, image)
try: # .ico
image = load_icon(ico, None)
except: # automatic img file type guessing
image = Image.open(ico)
# resize
if image.size[0] > 16:
image.resize((16, 16), Image.ANTIALIAS)
# .png format
image.save(png_fn, "PNG", quality=98)
# resize an image
def pngresize(fn, x=16, y=16):
image = Image.open(fn)
if image.size[0] > x:
image.resize((x, y), Image.ANTIALIAS)
image.save(fn, "PNG", quality=98)
#-- test
if __name__ == "__main__":
import sys
download(sys.argv[1])
|
Added http.py version [6932b3bfa3].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
#
# 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.
#
import urllib2
from urllib import urlencode
import config
from channels import __print__
#-- 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/0.4 (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
|
Added kronos.py version [6ae12b7565].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 |
"""Module that provides a cron-like task scheduler.
This task scheduler is designed to be used from inside your own program.
You can schedule Python functions to be called at specific intervals or
days. It uses the standard 'sched' module for the actual task scheduling,
but provides much more:
* repeated tasks (at intervals, or on specific days)
* error handling (exceptions in tasks don't kill the scheduler)
* optional to run scheduler in its own thread or separate process
* optional to run a task in its own thread or separate process
If the threading module is available, you can use the various Threaded
variants of the scheduler and associated tasks. If threading is not
available, you could still use the forked variants. If fork is also
not available, all processing is done in a single process, sequentially.
There are three Scheduler classes:
Scheduler ThreadedScheduler ForkedScheduler
You usually add new tasks to a scheduler using the add_interval_task or
add_daytime_task methods, with the appropriate processmethod argument
to select sequential, threaded or forked processing. NOTE: it is impossible
to add new tasks to a ForkedScheduler, after the scheduler has been started!
For more control you can use one of the following Task classes
and use schedule_task or schedule_task_abs:
IntervalTask ThreadedIntervalTask ForkedIntervalTask
SingleTask ThreadedSingleTask ForkedSingleTask
WeekdayTask ThreadedWeekdayTask ForkedWeekdayTask
MonthdayTask ThreadedMonthdayTask ForkedMonthdayTask
Kronos is the Greek God of Time.
Kronos scheduler (c) Irmen de Jong.
This version has been extracted from the Turbogears source repository
and slightly changed to be completely stand-alone again. Also some fixes
have been made to make it work on Python 2.6 (sched module changes).
The version in Turbogears is based on the original stand-alone Kronos.
This is open-source software, released under the MIT Software License:
http://www.opensource.org/licenses/mit-license.php
"""
__version__="2.0"
__all__ = [
"DayTaskRescheduler",
"ForkedIntervalTask",
"ForkedMonthdayTask",
"ForkedScheduler",
"ForkedSingleTask",
"ForkedTaskMixin",
"ForkedWeekdayTask",
"IntervalTask",
"MonthdayTask",
"Scheduler",
"SingleTask",
"Task",
"ThreadedIntervalTask",
"ThreadedMonthdayTask",
"ThreadedScheduler",
"ThreadedSingleTask",
"ThreadedTaskMixin",
"ThreadedWeekdayTask",
"WeekdayTask",
"add_interval_task",
"add_monthday_task",
"add_single_task",
"add_weekday_task",
"cancel",
"method",
]
import os
import sys
import sched
import time
import traceback
import weakref
class method:
sequential="sequential"
forked="forked"
threaded="threaded"
class Scheduler:
"""The Scheduler itself."""
def __init__(self):
self.running=True
self.sched = sched.scheduler(time.time, self.__delayfunc)
def __delayfunc(self, delay):
# This delay function is basically a time.sleep() that is
# divided up, so that we can check the self.running flag while delaying.
# there is an additional check in here to ensure that the top item of
# the queue hasn't changed
if delay<10:
time.sleep(delay)
else:
toptime = self._getqueuetoptime()
endtime = time.time() + delay
period = 5
stoptime = endtime - period
while self.running and stoptime > time.time() and \
self._getqueuetoptime() == toptime:
time.sleep(period)
if not self.running or self._getqueuetoptime() != toptime:
return
now = time.time()
if endtime > now:
time.sleep(endtime - now)
def _acquire_lock(self):
pass
def _release_lock(self):
pass
def add_interval_task(self, action, taskname, initialdelay, interval,
processmethod, args, kw):
"""Add a new Interval Task to the schedule.
A very short initialdelay or one of zero cannot be honored, you will
see a slight delay before the task is first executed. This is because
the scheduler needs to pick it up in its loop.
"""
if initialdelay < 0 or interval < 1:
raise ValueError("Delay or interval must be >0")
# Select the correct IntervalTask class. Not all types may be available!
if processmethod == method.sequential:
TaskClass = IntervalTask
elif processmethod == method.threaded:
TaskClass = ThreadedIntervalTask
elif processmethod == method.forked:
TaskClass = ForkedIntervalTask
else:
raise ValueError("Invalid processmethod")
if not args:
args = []
if not kw:
kw = {}
task = TaskClass(taskname, interval, action, args, kw)
self.schedule_task(task, initialdelay)
return task
def add_single_task(self, action, taskname, initialdelay, processmethod,
args, kw):
"""Add a new task to the scheduler that will only be executed once."""
if initialdelay < 0:
raise ValueError("Delay must be >0")
# Select the correct SingleTask class. Not all types may be available!
if processmethod == method.sequential:
TaskClass = SingleTask
elif processmethod == method.threaded:
TaskClass = ThreadedSingleTask
elif processmethod == method.forked:
TaskClass = ForkedSingleTask
else:
raise ValueError("Invalid processmethod")
if not args:
args = []
if not kw:
kw = {}
task = TaskClass(taskname, action, args, kw)
self.schedule_task(task, initialdelay)
return task
def add_daytime_task(self, action, taskname, weekdays, monthdays, timeonday,
processmethod, args, kw):
"""Add a new Day Task (Weekday or Monthday) to the schedule."""
if weekdays and monthdays:
raise ValueError("You can only specify weekdays or monthdays, "
"not both")
if not args:
args = []
if not kw:
kw = {}
if weekdays:
# Select the correct WeekdayTask class.
# Not all types may be available!
if processmethod == method.sequential:
TaskClass = WeekdayTask
elif processmethod == method.threaded:
TaskClass = ThreadedWeekdayTask
elif processmethod == method.forked:
TaskClass = ForkedWeekdayTask
else:
raise ValueError("Invalid processmethod")
task=TaskClass(taskname, weekdays, timeonday, action, args, kw)
if monthdays:
# Select the correct MonthdayTask class.
# Not all types may be available!
if processmethod == method.sequential:
TaskClass = MonthdayTask
elif processmethod == method.threaded:
TaskClass = ThreadedMonthdayTask
elif processmethod == method.forked:
TaskClass = ForkedMonthdayTask
else:
raise ValueError("Invalid processmethod")
task=TaskClass(taskname, monthdays, timeonday, action, args, kw)
firsttime=task.get_schedule_time(True)
self.schedule_task_abs(task, firsttime)
return task
def schedule_task(self, task, delay):
"""Add a new task to the scheduler with the given delay (seconds).
Low-level method for internal use.
"""
if self.running:
# lock the sched queue, if needed
self._acquire_lock()
try:
task.event = self.sched.enter(delay, 0, task,
(weakref.ref(self),) )
finally:
self._release_lock()
else:
task.event = self.sched.enter(delay, 0, task,
(weakref.ref(self),) )
def schedule_task_abs(self, task, abstime):
"""Add a new task to the scheduler for the given absolute time value.
Low-level method for internal use.
"""
if self.running:
# lock the sched queue, if needed
self._acquire_lock()
try:
task.event = self.sched.enterabs(abstime, 0, task,
(weakref.ref(self),) )
finally:
self._release_lock()
else:
task.event = self.sched.enterabs(abstime, 0, task,
(weakref.ref(self),) )
def start(self):
"""Start the scheduler."""
self._run()
def stop(self):
"""Remove all pending tasks and stop the Scheduler."""
self.running = False
self._clearschedqueue()
def cancel(self, task):
"""Cancel given scheduled task."""
self.sched.cancel(task.event)
if sys.version_info>=(2,6):
# code for sched module of python 2.6+
def _getqueuetoptime(self):
return self.sched._queue[0].time
def _clearschedqueue(self):
self.sched._queue[:] = []
else:
# code for sched module of python 2.5 and older
def _getqueuetoptime(self):
return self.sched.queue[0][0]
def _clearschedqueue(self):
self.sched.queue[:] = []
def _run(self):
# Low-level run method to do the actual scheduling loop.
while self.running:
try:
self.sched.run()
except Exception,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
if self.running:
time.sleep(5)
class Task:
"""Abstract base class of all scheduler tasks"""
def __init__(self, name, action, args, kw):
"""This is an abstract class!"""
self.name=name
self.action=action
self.args=args
self.kw=kw
def __call__(self, schedulerref):
"""Execute the task action in the scheduler's thread."""
try:
self.execute()
except Exception,x:
self.handle_exception(x)
self.reschedule(schedulerref())
def reschedule(self, scheduler):
"""This method should be defined in one of the sub classes!"""
raise NotImplementedError("You're using the abstract base class 'Task',"
" use a concrete class instead")
def execute(self):
"""Execute the actual task."""
self.action(*self.args, **self.kw)
def handle_exception(self, exc):
"""Handle any exception that occured during task execution."""
print >>sys.stderr, "ERROR DURING TASK EXECUTION", exc
print >>sys.stderr, "".join(traceback.format_exception(*sys.exc_info()))
print >>sys.stderr, "-" * 20
class SingleTask(Task):
"""A task that only runs once."""
def reschedule(self, scheduler):
pass
class IntervalTask(Task):
"""A repeated task that occurs at certain intervals (in seconds)."""
def __init__(self, name, interval, action, args=None, kw=None):
Task.__init__(self, name, action, args, kw)
self.interval = interval
def reschedule(self, scheduler):
"""Reschedule this task according to its interval (in seconds)."""
scheduler.schedule_task(self, self.interval)
class DayTaskRescheduler:
"""A mixin class that contains the reschedule logic for the DayTasks."""
def __init__(self, timeonday):
self.timeonday = timeonday
def get_schedule_time(self, today):
"""Calculate the time value at which this task is to be scheduled."""
now = list(time.localtime())
if today:
# schedule for today. let's see if that is still possible
if (now[3], now[4]) >= self.timeonday:
# too bad, it will be tomorrow
now[2] += 1
else:
# tomorrow
now[2] += 1
# set new time on day (hour,minute)
now[3], now[4] = self.timeonday
# seconds
now[5] = 0
return time.mktime(now)
def reschedule(self, scheduler):
"""Reschedule this task according to the daytime for the task.
The task is scheduled for tomorrow, for the given daytime.
"""
# (The execute method in the concrete Task classes will check
# if the current day is a day on which the task must run).
abstime = self.get_schedule_time(False)
scheduler.schedule_task_abs(self, abstime)
class WeekdayTask(DayTaskRescheduler, Task):
"""A task that is called at specific days in a week (1-7), at a fixed time
on the day.
"""
def __init__(self, name, weekdays, timeonday, action, args=None, kw=None):
if type(timeonday) not in (list, tuple) or len(timeonday) != 2:
raise TypeError("timeonday must be a 2-tuple (hour,minute)")
if type(weekdays) not in (list, tuple):
raise TypeError("weekdays must be a sequence of weekday numbers "
"1-7 (1 is Monday)")
DayTaskRescheduler.__init__(self, timeonday)
Task.__init__(self, name, action, args, kw)
self.days = weekdays
def execute(self):
# This is called every day, at the correct time. We only need to
# check if we should run this task today (this day of the week).
weekday = time.localtime().tm_wday + 1
if weekday in self.days:
self.action(*self.args, **self.kw)
class MonthdayTask(DayTaskRescheduler, Task):
"""A task that is called at specific days in a month (1-31), at a fixed
time on the day.
"""
def __init__(self, name, monthdays, timeonday, action, args=None, kw=None):
if type(timeonday) not in (list, tuple) or len(timeonday) != 2:
raise TypeError("timeonday must be a 2-tuple (hour,minute)")
if type(monthdays) not in (list, tuple):
raise TypeError("monthdays must be a sequence of monthdays numbers "
"1-31")
DayTaskRescheduler.__init__(self, timeonday)
Task.__init__(self, name, action, args, kw)
self.days = monthdays
def execute(self):
# This is called every day, at the correct time. We only need to
# check if we should run this task today (this day of the month).
if time.localtime().tm_mday in self.days:
self.action(*self.args, **self.kw)
try:
import threading
class ThreadedScheduler(Scheduler):
"""A Scheduler that runs in its own thread."""
def __init__(self):
Scheduler.__init__(self)
# we require a lock around the task queue
self._lock = threading.Lock()
def start(self):
"""Splice off a thread in which the scheduler will run."""
self.thread = threading.Thread(target=self._run)
self.thread.setDaemon(True)
self.thread.start()
def stop(self):
"""Stop the scheduler and wait for the thread to finish."""
Scheduler.stop(self)
try:
self.thread.join()
except AttributeError:
pass
def _acquire_lock(self):
"""Lock the thread's task queue."""
self._lock.acquire()
def _release_lock(self):
"""Release the lock on th ethread's task queue."""
self._lock.release()
class ThreadedTaskMixin:
"""A mixin class to make a Task execute in a separate thread."""
def __call__(self, schedulerref):
"""Execute the task action in its own thread."""
threading.Thread(target=self.threadedcall).start()
self.reschedule(schedulerref())
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:
self.handle_exception(x)
class ThreadedIntervalTask(ThreadedTaskMixin, IntervalTask):
"""Interval Task that executes in its own thread."""
pass
class ThreadedSingleTask(ThreadedTaskMixin, SingleTask):
"""Single Task that executes in its own thread."""
pass
class ThreadedWeekdayTask(ThreadedTaskMixin, WeekdayTask):
"""Weekday Task that executes in its own thread."""
pass
class ThreadedMonthdayTask(ThreadedTaskMixin, MonthdayTask):
"""Monthday Task that executes in its own thread."""
pass
except ImportError:
# threading is not available
pass
if hasattr(os, "fork"):
import signal
class ForkedScheduler(Scheduler):
"""A Scheduler that runs in its own forked process."""
def __del__(self):
if hasattr(self, "childpid"):
os.kill(self.childpid, signal.SIGKILL)
def start(self):
"""Fork off a new process in which the scheduler will run."""
pid = os.fork()
if pid == 0:
# we are the child
signal.signal(signal.SIGUSR1, self.signalhandler)
self._run()
os._exit(0)
else:
# we are the parent
self.childpid = pid
# can no longer insert in the scheduler queue
del self.sched
def stop(self):
"""Stop the scheduler and wait for the process to finish."""
os.kill(self.childpid, signal.SIGUSR1)
os.waitpid(self.childpid, 0)
def signalhandler(self, sig, stack):
Scheduler.stop(self)
class ForkedTaskMixin:
"""A mixin class to make a Task execute in a separate process."""
def __call__(self, schedulerref):
"""Execute the task action in its own process."""
pid = os.fork()
if pid == 0:
# we are the child
try:
self.execute()
except Exception,x:
self.handle_exception(x)
os._exit(0)
else:
# we are the parent
self.reschedule(schedulerref())
class ForkedIntervalTask(ForkedTaskMixin, IntervalTask):
"""Interval Task that executes in its own process."""
pass
class ForkedSingleTask(ForkedTaskMixin, SingleTask):
"""Single Task that executes in its own process."""
pass
class ForkedWeekdayTask(ForkedTaskMixin, WeekdayTask):
"""Weekday Task that executes in its own process."""
pass
class ForkedMonthdayTask(ForkedTaskMixin, MonthdayTask):
"""Monthday Task that executes in its own process."""
pass
if __name__=="__main__":
def testaction(arg):
print ">>>TASK",arg,"sleeping 3 seconds"
time.sleep(3)
print "<<<END_TASK",arg
s=ThreadedScheduler()
s.add_interval_task( testaction, "test action 1", 0, 4, method.threaded, ["task 1"], None )
s.start()
print "Scheduler started, waiting 15 sec...."
time.sleep(15)
print "STOP SCHEDULER"
s.stop()
print "EXITING"
|
Added mygtk.py version [8d9a2e155e].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
#
# encoding: UTF-8
# api: python
# type: functions
# title: mygtk helper functions
# description: simplify usage of some gtk widgets
# version: 1.5
# author: mario
# license: public domain
#
#
# Wrappers around gtk methods. The TreeView method .columns() allows
# to fill a treeview. It adds columns and data rows with a mapping
# dictionary (which specifies many options and data positions).
#
# The .tree() method is a trimmed-down variant of that, creates a
# single column, but has threaded entries.
#
# With the methodes .app_state() and .app_restore() named gtk widgets
# can be queried for attributes. The methods return a saveable dict,
# which contain current layout options for a few Widget types. Saving
# and restoring must be handled elsewhere.
#
#
# gtk modules
import pygtk
import gtk
import gtk.glade
import gobject
import os.path
import copy
# simplified gtk constructors ---------------------------------------------
class mygtk:
#-- fill a treeview
#
# Adds treeviewcolumns/cellrenderers and liststore from a data dictionary.
# Its datamap and the table contents can be supplied in one or two steps.
# When new data gets applied, the columns aren't recreated.
#
# The columns are created according to the datamap, which describes cell
# mapping and layout. Columns can have multiple cellrenderers, but usually
# there is a direct mapping to a data source key from entries.
#
# datamap = [ # title width dict-key type, renderer, attrs
# ["Name", 150, ["titlerow", str, "text", {} ] ],
# [False, 0, ["interndat", int, None, {} ] ],
# ["Desc", 200, ["descriptn", str, "text", {} ], ["icon",str,"pixbuf",{}] ],
#
# An according entries list then would contain a dictionary for each row:
# entries = [ {"titlerow":"first", "interndat":123}, {"titlerow":"..."}, ]
# Keys not mentioned in the datamap get ignored, and defaults are applied
# for missing cols. All values must already be in the correct type however.
#
@staticmethod
def columns(widget, datamap=[], entries=[], pix_entry=False, typecast=0):
# create treeviewcolumns?
if (not widget.get_column(0)):
# loop through titles
datapos = 0
for n_col,desc in enumerate(datamap):
# check for title
if (type(desc[0]) != str):
datapos += 1 # if there is none, this is just an undisplayed data column
continue
# new tvcolumn
col = gtk.TreeViewColumn(desc[0]) # title
col.set_resizable(True)
# width
if (desc[1] > 0):
col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
col.set_fixed_width(desc[1])
# loop through cells
for var in xrange(2, len(desc)):
cell = desc[var]
# cell renderer
if (cell[2] == "pixbuf"):
rend = gtk.CellRendererPixbuf() # img cell
if (cell[1] == str):
cell[3]["stock_id"] = datapos # for stock icons
expand = False
else:
pix_entry = datapos
cell[3]["pixbuf"] = datapos
else:
rend = gtk.CellRendererText() # text cell
cell[3]["text"] = datapos
#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():
col.add_attribute(rend, attr, val)
# next
datapos += 1
# add column to treeview
widget.append_column(col)
# finalize widget
widget.set_search_column(5) #??
widget.set_search_column(4) #??
widget.set_search_column(3) #??
widget.set_search_column(2) #??
widget.set_search_column(1) #??
#widget.set_reorderable(True)
# add data?
if (entries):
#- expand datamap
vartypes = [] #(str, str, bool, str, int, int, gtk.gdk.Pixbuf, str, int)
rowmap = [] #["title", "desc", "bookmarked", "name", "count", "max", "img", ...]
if (not rowmap):
for desc in datamap:
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
# prepare for missing values, and special variable types
defaults = {
str: "",
unicode: u"",
bool: False,
int: 0,
gtk.gdk.Pixbuf: gtk.gdk.pixbuf_new_from_data("\0\0\0\0",gtk.gdk.COLORSPACE_RGB,True,8,1,1,4)
}
if gtk.gdk.Pixbuf in vartypes:
pix_entry = vartypes.index(gtk.gdk.Pixbuf)
# sort data into gtk liststore array
for row in entries:
# generate ordered list from dictionary, using rowmap association
row = [ row.get( skey , defaults[vartypes[i]] ) for i,skey in enumerate(rowmap) ]
# 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:
# add
ls.append(row) # had to be adapted for real TreeStore (would require additional input for grouping/level/parents)
except:
# brute-force typecast
ls.append( [ty(va) for va,ty in zip(row,vartypes)] )
# apply array to widget
widget.set_model(ls)
return ls
pass
#-- treeview for categories
#
# simple two-level treeview display in one column
# with entries = [main,[sub,sub], title,[...],...]
#
@staticmethod
def tree(widget, entries, title="category", icon=gtk.STOCK_DIRECTORY):
# list types
ls = gtk.TreeStore(str, str)
# add entries
for entry in entries:
if (type(entry) == str):
main = ls.append(None, [entry, icon])
else:
for sub_title in entry:
ls.append(main, [sub_title, icon])
# just one column
tvcolumn = gtk.TreeViewColumn(title);
widget.append_column(tvcolumn)
# inner display: icon & string
pix = gtk.CellRendererPixbuf()
txt = gtk.CellRendererText()
# position
tvcolumn.pack_start(pix, expand=False)
tvcolumn.pack_end(txt, expand=True)
# select array content source in treestore
tvcolumn.set_attributes(pix, stock_id=1)
tvcolumn.set_attributes(txt, text=0)
# finalize
widget.set_model(ls)
tvcolumn.set_sort_column_id(0)
#tvcolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
#tvcolumn.set_fixed_width(125])
widget.set_search_column(0)
#widget.expand_all()
#widget.expand_row("3", False)
#print(widget.row_expanded("3"))
return ls
#-- save window size and widget properties
#
# needs a list of widgetnames
# e.g. pickle.dump(mygtk.app_state(...), open(os.environ["HOME"]+"/.config/app_winstate", "w"))
#
@staticmethod
def app_state(wTree, widgetnames=["window1", "treeview2", "vbox17"]):
r = {} # restore array
for wn in widgetnames:
r[wn] = {}
w = wTree.get_widget(wn)
t = type(w)
# print(wn, w, t)
# extract different information from individual widget types
if t == gtk.Window:
r[wn]["size"] = list(w.get_size())
#print("WINDOW SIZE", list(w.get_size()), r[wn])
if t == gtk.Widget:
r[wn]["name"] = w.get_name()
# gtk.TreeView
if t == gtk.TreeView:
r[wn]["columns:width"] = []
for col in w.get_columns():
r[wn]["columns:width"].append( col.get_width() )
# - Rows
r[wn]["rows:expanded"] = []
for i in xrange(0,50):
if w.row_expanded(str(i)):
r[wn]["rows:expanded"].append(i)
# - selected
(model, paths) = w.get_selection().get_selected_rows()
if paths:
r[wn]["row:selected"] = paths[0]
# gtk.Toolbar
if t == gtk.Toolbar:
r[wn]["icon_size"] = int(w.get_icon_size())
r[wn]["style"] = int(w.get_style())
# gtk.Notebook
if t == gtk.Notebook:
r[wn]["page"] = w.get_current_page()
#print(r)
return r
#-- restore window and widget properties
#
# requires only the previously saved widget state dict
#
@staticmethod
def app_restore(wTree, r=None):
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():
# gtk.Window
if method == "size":
w.resize(args[0], args[1])
# gtk.TreeView
if method == "columns:width":
for i,col in enumerate(w.get_columns()):
if (i < len(args)):
col.set_fixed_width(args[i])
# - Rows
if method == "rows:expanded":
w.collapse_all()
for i in args:
w.expand_row(str(i), False)
# - selected
if method == "row:selected":
w.get_selection().select_path(tuple(args))
# gtk.Toolbar
if method == "icon_size":
w.set_icon_size(args)
if method == "style":
w.set_style(args)
# gtk.Notebook
if method == "page":
w.set_current_page(args)
pass
#-- Save-As dialog
#
@staticmethod
def save_file(title="Save As", parent=None, fn="", formats=[("*","*")]):
c = gtk.FileChooserDialog(title, parent, action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=(gtk.STOCK_CANCEL, 0, gtk.STOCK_SAVE, 1))
# params
if fn:
c.set_current_name(fn)
fn = ""
for fname,ftype in formats:
f = gtk.FileFilter()
f.set_name(fname)
f.add_pattern(ftype)
c.add_filter(f)
# display
if c.run():
fn = c.get_filename() # return filaname
c.destroy()
return fn
# pass updates from another thread, ensures that it is called just once
@staticmethod
def do(lambda_func):
gobject.idle_add(lambda: lambda_func() and False)
# adds background color to widget,
# eventually wraps it into a gtk.Window, if it needs a container
@staticmethod
def bg(w, color="", where=["bg"]):
""" this method should be called after widget creation, and before .add()ing it to container """
if color:
# wrap unstylable widgets into EventBox
if not isinstance(w, gtk.Window):
wrap = gtk.EventBox()
wrap.add(w)
wrap.set_property("visible", True)
w = wrap
# copy style object, modify settings
s = w.get_style().copy()
c = w.get_colormap().alloc_color(color)
for state in (gtk.STATE_NORMAL, gtk.STATE_SELECTED):
s.bg[state] = c
w.set_style(s)
# probably redundant, but better safe than sorry:
w.modify_bg(gtk.STATE_NORMAL, c)
# return modified or wrapped widget
return w
@staticmethod
def add_menu(menuwidget, label, action):
m = gtk.MenuItem(label)
m.connect("activate", action)
m.show()
menuwidget.add(m)
# gtk.messagebox
@staticmethod
def msg(text, style=gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_CLOSE):
m = gtk.MessageDialog(None, 0, style, buttons, message_format=text)
m.show()
m.connect("response", lambda *w: m.destroy())
|
Added pq.py version [774d8a07cf].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#
# type: interface
# api: python
# title: PyQuery pq
# description: shortcut to PyQuery w/ extensions
#
#
import config
# load pyquery
try:
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:
# disable use
pq = None
config.conf.pyquery = False
# error hint
print("LXML is missing\n", e)
print("\n")
print("Please install the packages python-lxml and python-pyquery from your distributions software manager.\n")
# let's invoke packagekit?
"""
try:
import packagekit.client
pkc = packagekit.client.PackageKitClient()
pkc.install_packages([pkc.search_name(n) for n in ["python-lxml", "python-pyquery"]])
except:
print("no LXML")
"""
|
Added pson.py version [1f7ebfaebc].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
#
# 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) ----------------------------------
# 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 obj.iteritems():
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
|
Added st2.py version [70adb4776e].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 |
#!/usr/bin/env python
# encoding: UTF-8
# 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.8
# author: mario salzer
# license: public domain
# url: http://freshmeat.net/projects/streamtuner2
# config: <env name="http_proxy" value="" description="proxy for HTTP access" /> <env name="XDG_CONFIG_HOME" description="relocates user .config subdirectory" />
# category: multimedia
#
#
#
# Streamtuner2 is a GUI browser for internet radio directories. Various
# providers can be added, and streaming stations are usually grouped into
# music genres or categories. It starts external audio players for stream
# playing and streamripper for recording broadcasts.
#
# It's an independent rewrite of streamtuner1 in a scripting language. So
# it can be more easily extended and fixed. The use of PyQuery for HTML
# parsing makes this simpler and more robust.
#
# Stream lists are stored in JSON cache files.
#
#
#
""" project status """
#
# Cumulative development time is two months now, but the application
# runs mostly stable already. The GUI interfaces are workable.
# There haven't been any optimizations regarding memory usage and
# performance. The current internal API is acceptable. Documentation is
# coming up.
#
# 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/
#
# missing:
# - localization
#
# security notes:
# - directory scrapers use fragile regular expressions - which is probably
# not a security risk, but might lead to faulty data
# - MEDIUM: little integrity checking for .pls / .m3u references and files
# - minimal XML/SGML entity decoding (-> faulty data)
# - MEDIUM: if system json module is not available, pseudo-json uses eval()
# to read the config data -> limited risk, since it's only local files
# - HIGH RISK: no verification of downloaded favicon image files (ico/png),
# 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:
from threading import Thread
Thread.stop = lambda self: None
# gtk modules
import pygtk
import gtk
import gtk.glade
import gobject
# custom modules
sys.path.insert(0, "/usr/share/streamtuner2") # pre-defined directory for modules
sys.path.insert(0, ".") # pre-defined directory for modules
from config import conf # initializes itself, so all conf.vars are available right away
from mygtk import mygtk # gtk treeview
import http
import action # needs workaround... (action.main=main)
from channels import *
from channels import __print__
import favicon
#from pq import pq
# this represents the main window
# and also contains most application behaviour
main = None
class StreamTunerTwo(gtk.glade.XML):
# object containers
widgets = {} # non-glade widgets (the manually instantiated ones)
channels = {} # channel modules
features = {} # non-channel plugins
working = [] # threads
# status variables
channel_names = ["bookmarks"] # order of channel notebook tabs
current_channel = "bookmarks" # currently selected channel name (as index in self.channels{})
# constructor
def __init__(self):
# gtkrc stylesheet
self.load_theme(), gui_startup(0.05)
# instantiate gtk/glade widgets in current object
gtk.glade.XML.__init__(self, ("st2.glade" if os.path.exists("st2.glade") else conf.share+"/st2.glade")), gui_startup(0.10)
# manual gtk operations
self.extensionsCTM.set_submenu(self.extensions) # duplicates Station>Extension menu into stream context menu
# initialize channels
self.channels = {
"bookmarks": bookmarks(parent=self), # this the remaining built-in channel
"shoutcast": None,#shoutcast(parent=self),
}
gui_startup(0.15)
self.load_plugin_channels() # append other channel modules / plugins
# load application state (widget sizes, selections, etc.)
try:
winlayout = conf.load("window")
if (winlayout):
mygtk.app_restore(self, winlayout)
# selection values
winstate = conf.load("state")
if (winstate):
for id in winstate.keys():
self.channels[id].current = winstate[id]["current"]
self.channels[id].shown = winlayout[id+"_list"].get("row:selected", 0) # actually just used as boolean flag (for late loading of stream list), selection bar has been positioned before already
except:
pass # fails for disabled/reordered plugin channels
# display current open channel/notebook tab
gui_startup(0.90)
self.current_channel = self.current_channel_gtk()
try: self.channel().first_show()
except: print("channel .first_show() initialization error")
# bind gtk/glade event names to functions
gui_startup(0.95)
self.signal_autoconnect({
"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
"station_context_menu": lambda tv,ev: station_context_menu(tv,ev),
# toolbar
"on_play_clicked" : self.on_play_clicked,
"on_record_clicked": self.on_record_clicked,
"on_homepage_stream_clicked": self.on_homepage_stream_clicked,
"on_reload_clicked": self.on_reload_clicked,
"on_stop_clicked": self.on_stop_clicked,
"on_homepage_channel_clicked" : self.on_homepage_channel_clicked,
"double_click_channel_tab": self.on_homepage_channel_clicked,
# menu
"menu_toolbar_standard": lambda w: (self.toolbar.unset_style(), self.toolbar.unset_icon_size()),
"menu_toolbar_style_icons": lambda w: (self.toolbar.set_style(gtk.TOOLBAR_ICONS)),
"menu_toolbar_style_both": lambda w: (self.toolbar.set_style(gtk.TOOLBAR_BOTH)),
"menu_toolbar_size_small": lambda w: (self.toolbar.set_icon_size(gtk.ICON_SIZE_SMALL_TOOLBAR)),
"menu_toolbar_size_medium": lambda w: (self.toolbar.set_icon_size(gtk.ICON_SIZE_DND)),
"menu_toolbar_size_large": lambda w: (self.toolbar.set_icon_size(gtk.ICON_SIZE_DIALOG)),
# else
"menu_properties": config_dialog.open,
"config_cancel": config_dialog.hide,
"config_save": config_dialog.save,
"update_categories": self.update_categories,
"update_favicons": self.update_favicons,
"app_state": self.app_state,
"bookmark": self.bookmark,
"save_as": self.save_as,
"menu_about": lambda w: AboutStreamtuner2(),
"menu_help": action.action.help,
"menu_onlineforum": lambda w: action.browser("http://sourceforge.net/projects/streamtuner2/forums/forum/1173108"),
# "menu_bugreport": lambda w: BugReport(),
"menu_copy": self.menu_copy,
"delete_entry": self.delete_entry,
"quicksearch_set": search.quicksearch_set,
"search_open": search.menu_search,
"search_go": search.start,
"search_srv": search.start,
"search_google": search.google,
"search_cancel": search.cancel,
"true": lambda w,*args: True,
"streamedit_open": streamedit.open,
"streamedit_save": streamedit.save,
"streamedit_new": streamedit.new,
"streamedit_cancel": streamedit.cancel,
})
# actually display main window
gui_startup(0.99)
self.win_streamtuner2.show()
# WHY DON'T YOU WANT TO WORK?!
#self.shoutcast.gtk_list.set_enable_search(True)
#self.shoutcast.gtk_list.set_search_column(4)
#-- Shortcut fo 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)):
return self.channels[name] # like self.shoutcast
else:
return self.get_widget(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):
return self.widgets[name]
else:
return gtk.glade.XML.get_widget(self, name)
# returns the currently selected directory/channel object
def channel(self):
#try:
return self.channels[self.current_channel]
#except Exception,e:
# print(e)
# self.notebook_channels.set_current_page(0)
# self.current_channel = "bookmarks"
# return self.channels["bookmarks"]
def current_channel_gtk(self):
i = self.notebook_channels.get_current_page()
try: return self.channel_names[i]
except: return "bookmarks"
# notebook tab clicked
def channel_switch(self, notebook, page, page_num=0, *args):
# can be called from channelmenu as well:
if type(page) == str:
self.current_channel = page
self.notebook_channels.set_current_page(self.channel_names.index(page))
# notebook invocation:
else: #if type(page_num) == int:
self.current_channel = self.channel_names[page_num]
# if first selected, load current category
try:
print("try: .first_show", self.channel().module);
print(self.channel().first_show)
print(self.channel().first_show())
except:
print("channel .first_show() initialization error")
# convert ListStore iter to row number
def rowno(self):
(model, iter) = self.model_iter()
return model.get_path(iter)[0]
# currently selected entry in stations list, return complete data dict
def row(self):
return self.channel().stations() [self.rowno()]
# return ListStore object and Iterator for currently selected row in gtk.TreeView station list
def model_iter(self):
return self.channel().gtk_list.get_selection().get_selected()
# fetches a single varname from currently selected station entry
def selected(self, name="url"):
return self.row().get(name)
# play button
def on_play_clicked(self, widget, event=None, *args):
row = self.row()
if row:
self.channel().play(row)
favicon.download_playing(row)
# streamripper
def on_record_clicked(self, widget):
row = self.row()
action.record(row.get("url"), "audio/mp3", "url/direct", row=row)
# browse stream
def on_homepage_stream_clicked(self, widget):
url = self.selected("homepage")
action.browser(url)
# browse channel
def on_homepage_channel_clicked(self, widget, event=2):
if event == 2 or event.type == gtk.gdk._2BUTTON_PRESS:
__print__("dblclick")
action.browser(self.channel().homepage)
# reload stream list in current channel-category
def on_reload_clicked(self, widget=None, reload=1):
__print__("reload", reload, self.current_channel, self.channels[self.current_channel], self.channel().current)
category = self.channel().current
self.thread(
lambda: ( self.channel().load(category,reload), reload and self.bookmarks.heuristic_update(self.current_channel,category) )
)
# thread a function, add to worker pool (for utilizing stop button)
def thread(self, target, *args):
thread = Thread(target=target, args=args)
thread.start()
self.working.append(thread)
# stop reload/update threads
def on_stop_clicked(self, widget):
while self.working:
thread = self.working.pop()
thread.stop()
# click in category list
def on_category_clicked(self, widget, event, *more):
category = self.channel().currentcat()
__print__("on_category_clicked", category, self.current_channel)
self.on_reload_clicked(None, reload=0)
pass
# add current selection to bookmark store
def bookmark(self, widget):
self.bookmarks.add(self.row())
# code to update current list (set icon just in on-screen liststore, it would be updated with next display() anyhow - and there's no need to invalidate the ls cache, because that's referenced by model anyhow)
try:
(model,iter) = self.model_iter()
model.set_value(iter, 0, gtk.STOCK_ABOUT)
except:
pass
# refresh bookmarks tab
self.bookmarks.load(self.bookmarks.default)
# reload category tree
def update_categories(self, widget):
Thread(target=self.channel().reload_categories).start()
# menu invocation: refresh favicons for all stations in current streams category
def update_favicons(self, widget):
entries = self.channel().stations()
favicon.download_all(entries)
# save a file
def save_as(self, widget):
row = self.row()
default_fn = row["title"] + ".m3u"
fn = mygtk.save_file("Save Stream", None, default_fn, [(".m3u","*m3u"),(".pls","*pls"),(".xspf","*xspf"),(".smil","*smil"),(".asx","*asx"),("all files","*")])
if fn:
action.save(row, fn)
pass
# save current stream URL into clipboard
def menu_copy(self, w):
gtk.clipboard_get().set_text(self.selected("url"))
# remove an entry
def delete_entry(self, w):
n = self.rowno()
del self.channel().stations()[ n ]
self.channel().switch()
self.channel().save()
# stream right click
def station_context_menu(self, treeview, event):
return station_context_menu(treeview, event) # wrapper to the static function
# shortcut to statusbar
# (hacked to work from within threads, circumvents the statusbar msg pool actually)
def status(self, text="", sbar_msg=[]):
# init
sbar_cid = self.get_widget("statusbar").get_context_id("messages")
# remove text
while ((not text) and (type(text)==str) and len(sbar_msg)):
sbar_msg.pop()
mygtk.do(lambda:self.statusbar.pop(sbar_cid))
# progressbar
if (type(text)==float):
if (text >= 1.0): # completed
mygtk.do(lambda:self.progress.hide())
else: # show percentage
mygtk.do(lambda:self.progress.show() or self.progress.set_fraction(text))
if (text <= 0.0): # unknown state
mygtk.do(lambda:self.progress.pulse())
# add text
elif (type(text)==str):
sbar_msg.append(1)
mygtk.do(lambda:self.statusbar.push(sbar_cid, text))
pass
# load plugins from /usr/share/streamtuner2/channels/
def load_plugin_channels(self):
# find plugin files
ls = os.listdir(conf.share + "/channels/")
ls = [fn[:-3] for fn in ls if re.match("^[a-z][\w\d_]+\.py$", fn)]
# resort with tab order
order = [module.strip() for module in conf.channel_order.lower().replace(".","_").replace("-","_").split(",")]
ls = [module for module in (order) if (module in ls)] + [module for module in (ls) if (module not in order)]
# step through
for module in ls:
gui_startup(0.2 + 0.7 * float(ls.index(module))/len(ls), "loading module "+module)
# skip module if disabled
if conf.plugins.get(module, 1) == False:
__print__("disabled plugin:", module)
continue
# load plugin
try:
plugin = __import__("channels."+module, None, None, [""])
plugin_class = plugin.__dict__[module]
# load .config settings from plugin
conf.add_plugin_defaults(plugin_class.config, module)
# add and initialize channel
if issubclass(plugin_class, GenericChannel):
self.channels[module] = plugin_class(parent=self)
if module not in self.channel_names: # skip (glade) built-in channels
self.channel_names.append(module)
# other plugin types
else:
self.features[module] = plugin_class(parent=self)
except Exception, e:
print("error initializing:", module)
print(e)
# default plugins
conf.add_plugin_defaults(self.channels["bookmarks"].config, "bookmarks")
#conf.add_plugin_defaults(self.channels["shoutcast"].config, "shoutcast")
# store window/widget states (sizes, selections, etc.)
def app_state(self, widget):
# gtk widget states
widgetnames = ["win_streamtuner2", "toolbar", "notebook_channels", ] \
+ [id+"_list" for id in self.channel_names] + [id+"_cat" for id in self.channel_names]
conf.save("window", mygtk.app_state(wTree=self, widgetnames=widgetnames), nice=1)
# object vars
channelopts = {} #dict([(id, {"current":self.channels[id].current}) for id in self.channel_names])
for id in self.channels.keys():
if (self.channels[id]):
channelopts[id] = {"current":self.channels[id].current}
conf.save("state", channelopts, nice=1)
# apply gtkrc stylesheet
def load_theme(self):
if conf.get("theme"):
for dir in (conf.dir, conf.share, "/usr/share"):
f = dir + "/themes/" + conf.theme + "/gtk-2.0/gtkrc"
if os.path.exists(f):
gtk.rc_parse(f)
pass
# end application and gtk+ main loop
def gtk_main_quit(self, widget, *x):
if conf.auto_save_appstate:
self.app_state(widget)
gtk.main_quit()
# auxiliary window: about dialog
class AboutStreamtuner2:
# about us
def __init__(self):
a = gtk.AboutDialog()
a.set_version("2.0.8")
a.set_name("streamtuner2")
a.set_license("Public Domain\n\nNo Strings Attached.\nUnrestricted distribution,\nmodification, use.")
a.set_authors(["Mario Salzer <http://mario.include-once.org/>\n\nConcept based on streamtuner 0.99.99 from\nJean-Yves Lefort, of which some code remains\nin the Google stations plugin.\n<http://www.nongnu.org/streamtuner/>\n\nMyOggRadio plugin based on cooperation\nwith Christian Ehm. <http://ehm-edv.de/>"])
a.set_website("http://milki.include-once.org/streamtuner2/")
a.connect("response", lambda a, ok: ( a.hide(), a.destroy() ) )
a.show()
# right click in streams/stations TreeView
def station_context_menu(treeview, event):
# 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)
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
# encapsulates references to gtk objects AND properties in main window
class auxiliary_window(object):
def __getattr__(self, name):
if main.__dict__.has_key(name):
return main.__dict__[name]
elif StreamTunerTwo.__dict__.has_key(name):
return StreamTunerTwo.__dict__[name]
else:
return main.get_widget(name)
""" allows to use self. and main. almost interchangably """
# aux win: search dialog (keeps search text in self.q)
# and also: quick search textbox (uses main.q instead)
class search (auxiliary_window):
# show search dialog
def menu_search(self, w):
self.search_dialog.show();
# hide dialog box again
def cancel(self, *args):
self.search_dialog.hide()
return True # stop any other gtk handlers
#self.search_dialog.hide() #if conf.hide_searchdialog
# perform search
def start(self, *w):
self.cancel()
# prepare variables
self.q = self.search_full.get_text().lower()
entries = []
main.bookmarks.streams["search"] = []
# which fields?
fields = ["title", "playing", "genre", "homepage", "url", "extra", "favicon", "format"]
if not self.search_in_all.get_active():
fields = [f for f in fields if (main.get_widget("search_in_"+f) and main.get_widget("search_in_"+f).get_active())]
# channels?
channels = main.channel_names[:]
if not self.search_channel_all.get_active():
channels = [c for c in channels if main.get_widget("search_channel_"+c).get_active()]
# step through channels
for c in channels:
if main.channels[c] and main.channels[c].streams: # skip disabled plugins
# categories
for cat in main.channels[c].streams.keys():
# stations
for row in main.channels[c].streams[cat]:
# assemble text fields
text = " ".join([row.get(f, " ") for f in fields])
# compare
if text.lower().find(self.q) >= 0:
# add result
entries.append(row)
# display "search" in "bookmarks"
main.channel_switch(None, "bookmarks", 0)
main.bookmarks.set_category("search")
# insert data and show
main.channels["bookmarks"].streams["search"] = entries # we have to set it here, else .currentcat() might reset it
main.bookmarks.load("search")
# live search on directory server homepages
def server_query(self, w):
"unimplemented"
# don't search at all, open a web browser
def google(self, w):
self.cancel()
action.browser("http://www.google.com/search?q=" + self.search_full.get_text())
# search text edited in text entry box
def quicksearch_set(self, w, *eat, **up):
# keep query string
main.q = self.search_quick.get_text().lower()
# get streams
c = main.channel()
rows = c.stations()
col = c.rowmap.index("search_col") # this is the gtk.ListStore index # which contains the highlighting color
# callback to compare (+highlight) rows
m = c.gtk_list.get_model()
m.foreach(self.quicksearch_treestore, (rows, main.q, col, col+1))
search_set = quicksearch_set
# callback that iterates over whole gtk treelist,
# looks for search string and applies TreeList color and flag if found
def quicksearch_treestore(self, model, path, iter, extra_data):
i = path[0]
(rows, q, color, flag) = extra_data
# compare against interesting content fields:
text = rows[i].get("title", "") + " " + rows[i].get("homepage", "")
# config.quicksearch_fields
text = text.lower()
# simple string match (probably doesn't need full search expression support)
if len(q) and text.find(q) >= 0:
model.set_value(iter, color, "#fe9") # highlighting color
model.set_value(iter, flag, True) # background-set flag
# color = 12 in liststore, flag = 13th position
else:
model.set_value(iter, color, "") # for some reason the cellrenderer colors get applied to all rows, even if we specify an iter (=treelist position?, not?)
model.set_value(iter, flag, False) # that's why we need the secondary -set option
#??
return False
search = search()
# instantiates itself
# aux win: stream data editing dialog
class streamedit (auxiliary_window):
# show stream data editing dialog
def open(self, mw):
row = main.row()
for name in ("title", "playing", "genre", "homepage", "url", "favicon", "format", "extra"):
w = main.get_widget("streamedit_" + name)
if w:
w.set_text((str(row.get(name)) if row.get(name) else ""))
self.win_streamedit.show()
# copy widget contents to stream
def save(self, w):
row = main.row()
for name in ("title", "playing", "genre", "homepage", "url", "favicon", "format", "extra"):
w = main.get_widget("streamedit_" + name)
if w:
row[name] = w.get_text()
main.channel().save()
self.cancel(w)
# add a new list entry, update window
def new(self, w):
s = main.channel().stations()
s.append({"title":"new", "url":"", "format":"audio/mp3", "genre":"", "listeners":1});
main.channel().switch() # update display
main.channel().gtk_list.get_selection().select_path(str(len(s)-1)); # set cursor to last row
self.open(w)
# hide window
def cancel(self, *w):
self.win_streamedit.hide()
return True
streamedit = streamedit()
# instantiates itself
# aux win: settings UI
class config_dialog (auxiliary_window):
# display win_config, pre-fill text fields from global conf. object
def open(self, widget):
self.add_plugins()
self.apply(conf.__dict__, "config_", 0)
#self.win_config.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#443399'))
self.combobox_theme()
self.win_config.show()
def hide(self, *args):
self.win_config.hide()
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():
# 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__("config_save", save, prefix+id, w, val)
# recurse into dictionaries, transform: conf.play.audio/mp3 => conf.play_audio_mp3
if (type(val) == dict):
self.apply(val, prefix + id + "_", save)
# load or set gtk.Entry text field
elif (w and save and type(w)==gtk.Entry):
config[key] = w.get_text()
elif (w and type(w)==gtk.Entry):
w.set_text(str(val))
elif (w and save):
config[key] = w.get_active()
elif (w):
w.set_active(bool(val))
pass
# fill combobox
def combobox_theme(self):
# self.theme.combo_box_new_text()
# find themes
themedirs = (conf.share+"/themes", conf.dir+"/themes", "/usr/share/themes")
themes = ["no theme"]
[[themes.append(e) for e in os.listdir(dir)] for dir in themedirs if os.path.exists(dir)]
# add to combobox
for num,themename in enumerate(themes):
self.theme.append_text(themename)
if conf.theme == themename:
self.theme.set_active(num)
# erase this function, so it only ever gets called once
self.combobox_theme = lambda: None
# retrieve currently selected value
def apply_theme(self):
if self.theme.get_active() >= 0:
conf.theme = self.theme.get_model()[ self.theme.get_active()][0]
main.load_theme()
# add configuration setting definitions from plugins
once = 0
def add_plugins(self):
if self.once:
return
for name,enabled in conf.plugins.iteritems():
# 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)
self.add_( "config_plugins_"+name, cb )#, label=None, color="#ddd" )
# look up individual plugin options, if loaded
if self.channels.get(name) or self.features.get(name):
c = self.channels.get(name) or self.features.get(name)
for opt in c.config:
# default values are already in conf[] dict (now done in conf.add_plugin_defaults)
# display checkbox or text entry
if opt["type"] == "boolean":
cb = gtk.CheckButton(opt["description"])
#cb.set_line_wrap(True)
self.add_( "config_"+opt["name"], cb )
else:
self.add_( "config_"+opt["name"], gtk.Entry(), opt["description"] )
# spacer
self.add_( "filler_pl_"+name, gtk.HSeparator() )
self.once = 1
# put gtk widgets into config dialog notebook
def add_(self, id, w, label=None, color=""):
w.set_property("visible", True)
main.widgets[id] = w
if label:
w.set_width_chars(10)
label = gtk.Label(label)
label.set_property("visible", True)
label.set_line_wrap(True)
label.set_size_request(250, -1)
vbox = gtk.HBox(homogeneous=False, spacing=10)
vbox.set_property("visible", True)
vbox.pack_start(w, expand=False, fill=False)
vbox.pack_start(label, expand=True, fill=True)
w = vbox
if color:
w = mygtk.bg(w, color)
self.plugin_options.pack_start(w)
# save config
def save(self, widget):
self.apply(conf.__dict__, "config_", 1)
self.apply_theme()
conf.save(nice=1)
self.hide()
config_dialog = config_dialog()
# instantiates itself
# class GenericChannel:
#
# is in channels/__init__.py
#
#-- favourite lists ------------------------------------------
#
# This module lists static content from ~/.config/streamtuner2/bookmarks.json;
# its data list is queried by other plugins to add 'star' icons.
#
# Some feature extensions inject custom categories[] into streams{}
# e.g. "search" adds its own category once activated, as does the "timer" plugin.
#
class bookmarks(GenericChannel):
# desc
api = "streamtuner2"
module = "bookmarks"
title = "bookmarks"
version = 0.4
base_url = "file:.config/streamtuner2/bookmarks.json"
listformat = "*/*"
# i like this
config = [
{"name":"like_my_bookmarks", "type":"boolean", "value":0, "description":"I like my bookmarks"},
]
# content
categories = ["favourite", ]
current = "favourite"
default = "favourite"
streams = {"favourite":[], "search":[], "scripts":[], "timer":[], }
# cache list, to determine if a PLS url is bookmarked
urls = []
# this channel does not actually retrieve/parse data from anywhere
def update_categories(self):
pass
def update_streams(self, cat):
return self.streams.get(cat, [])
# initial display
def first_show(self):
if not self.streams["favourite"]:
self.cache()
# all entries just come from "bookmarks.json"
def cache(self):
# stream list
cache = conf.load(self.module)
if (cache):
self.streams = cache
# save to cache file
def save(self):
conf.save(self.module, self.streams, nice=1)
# checks for existence of an URL in bookmarks store,
# this method is called by other channel modules' display() method
def is_in(self, url, once=1):
if (not self.urls):
self.urls = [row.get("url","urn:x-streamtuner2:no") for row in self.streams["favourite"]]
return url in self.urls
# called from main window / menu / context menu,
# when bookmark is to be added for a selected stream entry
def add(self, row):
# normalize data (this row originated in a gtk+ widget)
row["favourite"] = 1
if row.get("favicon"):
row["favicon"] = favicon.file(row.get("homepage"))
if not row.get("listformat"):
row["listformat"] = main.channel().listformat
# append to storage
self.streams["favourite"].append(row)
self.save()
self.load(self.default)
self.urls.append(row["url"])
# simplified gtk TreeStore display logic (just one category for the moment, always rebuilt)
def load(self, category, force=False):
#self.liststore[category] = \
# print(category, self.streams.keys())
mygtk.columns(self.gtk_list, self.datamap, self.prepare(self.streams.get(category,[])))
# select a category in treeview
def add_category(self, cat):
if cat not in self.categories: # add category if missing
self.categories.append(cat)
self.display_categories()
# change cursor
def set_category(self, cat):
self.add_category(cat)
self.gtk_cat.get_selection().select_path(str(self.categories.index(cat)))
return self.currentcat()
# update bookmarks from freshly loaded streams data
def heuristic_update(self, updated_channel, updated_category):
if not conf.heuristic_bookmark_update: return
save = 0
fav = self.streams["favourite"]
# First we'll generate a list of current bookmark stream urls, and then
# remove all but those from the currently UPDATED_channel + category.
# 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():
# keep the potentially changed rows
if (chname == updated_channel) and (cat == updated_category):
freshened_streams = streams
# remove unchanged urls/rows
else:
unchanged_urls = (row.get("url") for row in streams)
for url in unchanged_urls:
if url in check:
del check[url]
# directory duplicates could unset the check list here,
# so we later end up doing a deep comparison
# now the real comparison,
# where we compare station titles and homepage url to detect if a bookmark is an old entry
for row in freshened_streams:
url = row.get("url")
# empty entry (google stations), or stream still in current favourites
if not url or url in check:
pass
# need to search
else:
title = row.get("title")
homepage = row.get("homepage")
for i,old in enumerate(fav):
# skip if new url already in streams
if url == old.get("url"):
pass # This is caused by channel duplicates with identical PLS links.
# on exact matches (but skip if url is identical anyway)
elif title == old["title"] and homepage == old.get("homepage",homepage):
# update stream url
fav[i]["url"] = url
save = 1
# more text similarity heuristics might go here
else:
pass
# if there were changes
if save: self.save()
#-- startup progress bar
progresswin, progressbar = 0, 0
def gui_startup(p=0.0, msg="streamtuner2 is starting"):
global progresswin,progressbar
if not progresswin:
# GtkWindow "progresswin"
progresswin = gtk.Window()
progresswin.set_property("title", "streamtuner2")
progresswin.set_property("default_width", 300)
progresswin.set_property("width_request", 300)
progresswin.set_property("default_height", 30)
progresswin.set_property("height_request", 30)
progresswin.set_property("window_position", "center")
progresswin.set_property("decorated", False)
progresswin.set_property("visible", True)
# GtkProgressBar "progressbar"
progressbar = gtk.ProgressBar()
progressbar.set_property("visible", True)
progressbar.set_property("show_text", True)
progressbar.set_property("text", msg)
progresswin.add(progressbar)
progresswin.show_all()
try:
if p<1:
progressbar.set_fraction(p)
progressbar.set_property("text", msg)
while gtk.events_pending(): gtk.main_iteration(False)
else:
progresswin.destroy()
except: return
#-- run main ---------------------------------------------
if __name__ == "__main__":
#-- global configuration settings
"conf = Config()" # already happened with "from config import conf"
# graphical
if len(sys.argv) < 2:
# prepare for threading in Gtk+ callbacks
gobject.threads_init()
gui_startup(0.05)
# prepare main window
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
# first invocation
if (conf.get("firstrun")):
config_dialog.open(None)
del conf.firstrun
# run
gui_startup(1.00)
gtk.main()
# invoke command-line interface
else:
import cli
cli.StreamTunerCLI()
#
#
#
#
|