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() # # # # |