diff -Nru streamtuner2-2.1.1/ahttp.py streamtuner2-2.1.3/ahttp.py --- streamtuner2-2.1.1/ahttp.py 2014-05-26 14:00:33.000000000 +0000 +++ streamtuner2-2.1.3/ahttp.py 2014-07-31 16:47:52.000000000 +0000 @@ -44,14 +44,10 @@ # default HTTP headers for requests session.headers.update({ "User-Agent": "streamtuner2/2.1 (X11; U; Linux AMD64; en; rv:1.5.0.1) like WinAmp/2.1", - "Accept": "*/*;q=0.5, audio/*, video/*, json/*, url/*", + "Accept": "*/*", "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": "close", - "Pragma": "no-cache", - "Cache-Control": "no-cache", + "Accept-Encoding": "gzip, deflate", + "Accept-Charset": "UTF-8, ISO-8859-1;q=0.5, *;q=0.1", }) @@ -60,7 +56,7 @@ # # Well, it says "get", but it actually does POST and AJAXish GET requests too. # -def get(url, params={}, referer="", post=0, ajax=0, binary=0, feedback=None): +def get(url, params={}, referer="", post=0, ajax=0, binary=0, feedback=None, content=True): __print__( dbg.HTTP, "GET", url, params ) # statusbar info @@ -79,17 +75,21 @@ else: r = session.get(url, params=params, headers=headers) - #__print__( dbg.HTTP, r.request.headers ); - #__print__( dbg.HTTP, r.headers ); + __print__( dbg.HTTP, r.request.headers ); + __print__( dbg.HTTP, r.headers ); - # result - #progress_feedback(0.9) - content = (r.content if binary else r.text) - # finish, clean statusbar + #progress_feedback(0.9) progress_feedback("") - __print__( dbg.INFO, "Content-Length", len(content) ) - return content + + # result + __print__( dbg.INFO, "Content-Length", len(r.content) ) + if not content: + return r + elif binary: + return r.content + else: + return r.text Binary files /tmp/X5fFoJRG69/streamtuner2-2.1.1/channels/dirble.png and /tmp/8t7mX63Ede/streamtuner2-2.1.3/channels/dirble.png differ diff -Nru streamtuner2-2.1.1/channels/dirble.py streamtuner2-2.1.3/channels/dirble.py --- streamtuner2-2.1.1/channels/dirble.py 1970-01-01 00:00:00.000000000 +0000 +++ streamtuner2-2.1.3/channels/dirble.py 2014-08-12 18:45:26.000000000 +0000 @@ -0,0 +1,127 @@ +# encoding: UTF-8 +# api: streamtuner2 +# title: Dirble +# description: Open radio station directory. +# version: 0.2 +# type: channel +# category: radio +# priority: optional +# documentation: http://dirble.com/developer/api +# +# Provides a nice JSON API, so is easy to support. +# +# However useful station information (homepage, etc.) only +# with extraneous requests. So just for testing as of now. +# +# Uh, and API is appearently becoming for-pay (two days +# after writing this plugin;). So ST2 users may have to +# request their own Dirble.com key probably. +# + + +import re +import json +from config import conf, dbg, __print__ +from channels import * +import ahttp as http + + +# Surfmusik sharing site +class dirble (ChannelPlugin): + + # description + title = "Dirble" + module = "dirble" + homepage = "http://dirble.com/" + has_search = True + listformat = "audio/x-scpls" + titles = dict(listeners=False, playing="Location") + + categories = [] + config = [ + {"name": "dirble_api_key", + "value": "", + "type": "text", + "description": "Custom API access key." + }, + {"name": "dirble_fetch_homepage", + "value": 0, + "type": "boolean", + "description": "Also fetch homepages when updating stations. (This is slow, as it requires one extra request for each.)" + } + ] + catmap = {} + + base = "http://api.dirble.com/v1/%s/apikey/%s/" + cid = "84be582567ff418c9ba94d90d075d7fee178ad60" + + + # Retrieve cat list and map + def update_categories(self): + self.categories = [] + # Main categories + for row in self.api("primaryCategories"): + self.categories.append(row["name"]) + self.catmap[row["name"]] = row["id"] + # Request subcats + sub = [] + self.categories.append(sub) + for subrow in self.api("childCategories", "primaryid", row["id"]): + sub.append(subrow["name"]) + self.catmap[subrow["name"]] = subrow["id"] + + + # Just copy over stream URLs and station titles + def update_streams(self, cat, search=None): + + if cat: + id = self.catmap.get(cat, 0); + data = self.api("stations", "id", id) + elif search: + data = self.api("search", "search", search) + else: + pass + + r = [] + for e in data: + # skip musicgoal (resolve to just a blocking teaser) + if e["streamurl"].find("musicgoal") > 0: + continue + # append dict after renaming fields + r.append(dict( + id = e["id"], + genre = str(cat), + status = e["status"], + title = e["name"], + playing = e["country"], + bitrate = self.to_int(e["bitrate"]), + url = e["streamurl"], + homepage = e.get("homepage") or self.get_homepage(e["id"], e["name"]), + format = "audio/mpeg" + )) + return r + + + # Request homepage for stations, else try to deduce Dirble page + def get_homepage(self, id, name): + if conf.dirble_fetch_homepage: + try: + return self.api("station", "id", id)["website"] + except: + None + name = re.sub("[^\w\s]+", "", name) + name = re.sub("\s", "-", name) + return "http://dirble.com/station/" + name.lower(); + + + # Patch API url together, send request, decode JSON and whathaveyou + def api(self, *params): + method = params[0] + try: + j = http.get((self.base % (method, conf.dirble_api_key or self.cid)) + "/".join([str(e) for e in params[1:]])) + r = json.loads(j); + except: + r = [] + return r + + diff -Nru streamtuner2-2.1.1/channels/file.py streamtuner2-2.1.3/channels/file.py --- streamtuner2-2.1.1/channels/file.py 2014-05-13 16:20:41.000000000 +0000 +++ streamtuner2-2.1.3/channels/file.py 2014-07-24 23:15:59.000000000 +0000 @@ -6,7 +6,7 @@ # category: media # version: 0.0 # priority: optional -# depends: mutagen, kiwi +# depends: mutagen # # # Local file browser. @@ -180,6 +180,8 @@ # same as init def update_streams(self, cat, x=0): self.scan_dirs() + print(self.streams) + print(self.categories) return self.streams.get(os.path.basename(cat)) diff -Nru streamtuner2-2.1.1/channels/_generic.py streamtuner2-2.1.3/channels/_generic.py --- streamtuner2-2.1.1/channels/_generic.py 2014-05-26 14:47:35.000000000 +0000 +++ streamtuner2-2.1.3/channels/_generic.py 2014-08-01 01:10:55.000000000 +0000 @@ -49,7 +49,7 @@ # desc module = "generic" title = "GenericChannel" - homepage = "http://milki.inlcude-once.org/streamtuner2/" + homepage = "http://fossil.include-once.org/streamtuner2/" base_url = "" listformat = "audio/x-scpls" audioformat = "audio/mpeg" # fallback value @@ -58,6 +58,7 @@ # categories categories = ["empty", ] + catmap = {} 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 @@ -133,6 +134,10 @@ cache = conf.load("cache/categories_" + self.module) if (cache): self.categories = cache + # catmap (optional) + cache = conf.load("cache/catmap_" + self.module) + if (cache): + self.catmap = cache pass @@ -332,7 +337,12 @@ self.load(self.current, force=1) def switch(self): self.load(self.current, force=0) - + + # update streams pane if currently selected (used by bookmarks.links channel) + def reload_if_current(self, category): + if self.current == category: + self.reload() + # display .current category, once notebook/channel tab is first opened def first_show(self): @@ -371,7 +381,10 @@ # get data and save self.update_categories() - conf.save("cache/categories_"+self.module, self.categories) + if self.categories: + conf.save("cache/categories_"+self.module, self.categories) + if self.catmap: + conf.save("cache/catmap_" + self.module, self.catmap); # display outside of this non-main thread mygtk.do(self.display_categories) @@ -459,6 +472,11 @@ # convert special characters to &xx; escapes def xmlentities(self, s): return xml.sax.saxutils.escape(s) + + # Extracts integer from string + def to_int(self, s): + i = re.findall("\d+", s) or [0] + return int(i[0]) diff -Nru streamtuner2-2.1.1/channels/history.py streamtuner2-2.1.3/channels/history.py --- streamtuner2-2.1.1/channels/history.py 2014-05-27 01:38:53.000000000 +0000 +++ streamtuner2-2.1.3/channels/history.py 2014-06-01 23:33:01.000000000 +0000 @@ -50,6 +50,7 @@ # create category self.bm.add_category("history"); + self.bm.reload_if_current(self.module) # hook up to .play event parent.hooks["play"].append(self.queue) @@ -75,6 +76,8 @@ # update store self.bm.save() - #if self.bm.current == "history": - # self.bm.load("history") + self.bm.reload_if_current(self.module) + + + Binary files /tmp/X5fFoJRG69/streamtuner2-2.1.1/channels/icast.png and /tmp/8t7mX63Ede/streamtuner2-2.1.3/channels/icast.png differ diff -Nru streamtuner2-2.1.1/channels/icast.py streamtuner2-2.1.3/channels/icast.py --- streamtuner2-2.1.1/channels/icast.py 1970-01-01 00:00:00.000000000 +0000 +++ streamtuner2-2.1.3/channels/icast.py 2014-05-30 23:20:07.000000000 +0000 @@ -0,0 +1,88 @@ +# encoding: UTF-8 +# api: streamtuner2 +# title: iCast +# description: Open collaborative stream directory +# version: 0.1 +# type: channel +# category: radio +# priority: optional +# documentation: http://api.icast.io/ +# +# A modern alternative to ShoutCast/ICEcast. +# Streams are user-contributed, but often lack meta data (homepage) and +# there's no ordering by listeneres/popularity. +# +# OTOH it's every easy to interface with. Though the repeated API queries +# due to 10-entries-per-query results make fetching slow. +# +# +# + +import re +import json +from config import conf, dbg, __print__ +from channels import * +import ahttp as http + + +# Surfmusik sharing site +class icast (ChannelPlugin): + + # description + title = "iCast" + module = "icast" + homepage = "http://www.icast.io/" + has_search = True + listformat = "audio/x-scpls" + titles = dict(listeners=False, bitrate=False, playing=False) + + categories = [] + config = [ + ] + + base = "http://api.icast.io/1/" + + + # Categories require little post-processing, just dict into list conversion + def update_categories(self): + self.categories = [] + for genre,cats in json.loads(http.get(self.base + "genres"))["genres"].items(): + self.categories.append(genre.title()) + self.categories.append([c.title() for c in cats]) + + # Just copy over stream URLs and station titles + def update_streams(self, cat, search=None): + + if cat: + data = self.api("stations/genre/", cat.lower(), {}) + elif search: + data = self.api("stations/search", "", {"q": search}) + else: + pass + + r = [] + for e in data: + r.append(dict( + genre = " ".join(e["genre_list"]), + url = e["streams"][0]["uri"], + format = e["streams"][0]["mime"], + title = e["name"], + #playing = " ".join(e["current"].items()), + )) + + return r + + # fetch multiple pages + def api(self, method, path, params): + r = [] + while len(r) < int(conf.max_streams): + data = json.loads(http.get( self.base + method + path, params)) + r += data["stations"] + if len(r) >= data["meta"]["total_count"] or len(data["stations"]) < 10: + break + else: + params["page"] = int(data["meta"]["current_page"]) + 1 + self.parent.status(params["page"] * 9.5 / float(conf.max_streams)) + #__print__(dbg.DATA, data) + return r + diff -Nru streamtuner2-2.1.1/channels/internet_radio.py streamtuner2-2.1.3/channels/internet_radio.py --- streamtuner2-2.1.1/channels/internet_radio.py 2014-05-28 16:29:47.000000000 +0000 +++ streamtuner2-2.1.3/channels/internet_radio.py 2014-06-02 00:22:20.000000000 +0000 @@ -69,7 +69,7 @@ # fetch station lists - def update_streams(self, cat, force=0): + def update_streams(self, cat): entries = [] if cat not in self.categories: @@ -229,12 +229,12 @@ # transform data r.append({ "url": url, - "genre": self.strip_tags(genres), - "homepage": http.fix_url(homepage), - "title": (title if title else "").strip(), - "playing": (playing if playing else "").strip(), - "bitrate": int(bitrate if bitrate else 0), - "listeners": int(listeners if listeners else 0), + "genre": self.strip_tags(genres or ""), + "homepage": http.fix_url(homepage or ""), + "title": (title or "").strip().replace("\n", " "), + "playing": (playing or "").strip().replace("\n", " "), + "bitrate": int(bitrate or 0), + "listeners": int(listeners or 0), "format": "audio/mpeg", # 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 }) else: Binary files /tmp/X5fFoJRG69/streamtuner2-2.1.1/channels/itunes.png and /tmp/8t7mX63Ede/streamtuner2-2.1.3/channels/itunes.png differ diff -Nru streamtuner2-2.1.1/channels/itunes.py streamtuner2-2.1.3/channels/itunes.py --- streamtuner2-2.1.1/channels/itunes.py 1970-01-01 00:00:00.000000000 +0000 +++ streamtuner2-2.1.3/channels/itunes.py 2014-06-03 00:17:11.000000000 +0000 @@ -0,0 +1,105 @@ +# encoding: UTF-8 +# api: streamtuner2 +# title: iTunes Radio (via RS) +# description: iTunes unsorted station list via RoliSoft Radio Playlist caching webservice. +# version: 0.1 +# type: channel +# category: radio +# priority: optional +# documentation: http://lab.rolisoft.net/playlists.html +# +# Provides pre-parsed radio station playlists for various services +# → Shoutcast +# → Xiph/ICEcast +# → Tunein +# → iTunes +# → FilterMusic +# → SomaFM +# → AccuRadio +# → BBC +# +# In this module only iTunes will be queried for now. +# +# + +import re +from config import conf, dbg, __print__ +from channels import * +import ahttp as http + + +# Surfmusik sharing site +class itunes (ChannelPlugin): + + # description + title = "iTunes RS" + module = "itunes" + #module = "rs_playlist" + homepage = "http://www.itunes.com?" + has_search = False + listformat = "audio/x-scpls" + titles = dict(listeners=False, bitrate=False, playing=False) + + categories = [ + "Adult Contemporary", + "Alternative Rock", + "Ambient", + "Blues", + "Classic Rock", + "Classical", + "College", + "Comedy", + "Country", + "Eclectic", + "Electronica", + "Golden Oldies", + "Hard Rock", + "Hip Hop", + "International", + "Jazz", + "News", + "Raggae", + "Religious", + "RnB", + "Sports Radio", + "Top 40", + "'70s Retro", + "'80s Flashback", + "'90s Hits", + ] + config = [ + ] + + base = "http://lab.rolisoft.net/playlists/itunes.php" + #base = "http://aws-eu.rolisoft.net/playlists/itunes.php" + #base = "http://aws-us.rolisoft.net/playlists/itunes.php" + + + # static list for iTunes + def update_categories(self): + pass + + # Just copy over stream URLs and station titles + def update_streams(self, cat): + + m3u = http.get(self.base, {"category": cat.lower()}) + if len(m3u) < 256: + __print__(dbg.ERR, m3u) + + rx_m3u = re.compile(r""" + ^File(\d+)\s*=\s*(http://[^\s]+)\s*$\s* + ^Title\1\s*=\s*([^\r\n]+)\s*$\s* + """, re.M|re.I|re.X) + + r = [] + for e in rx_m3u.findall(m3u): + r.append(dict( + genre = cat, + url = e[1], + title = e[2], + format = "audio/mpeg", + playing = "", + )) + + return r + diff -Nru streamtuner2-2.1.1/channels/jamendo.py streamtuner2-2.1.3/channels/jamendo.py --- streamtuner2-2.1.1/channels/jamendo.py 2014-05-27 21:35:34.000000000 +0000 +++ streamtuner2-2.1.3/channels/jamendo.py 2014-06-02 00:22:38.000000000 +0000 @@ -277,7 +277,7 @@ # retrieve category or search - def update_streams(self, cat, search="", force=0): + def update_streams(self, cat, search=None): entries = [] fmt = self.stream_mime(conf.jamendo_stream_format) diff -Nru streamtuner2-2.1.1/channels/links.py streamtuner2-2.1.3/channels/links.py --- streamtuner2-2.1.1/channels/links.py 2014-05-13 16:21:35.000000000 +0000 +++ streamtuner2-2.1.3/channels/links.py 2014-06-01 23:29:53.000000000 +0000 @@ -4,7 +4,7 @@ # description: Static list of various music directory websites. # type: category # category: web -# version: 0.1 +# version: 0.2 # priority: default # # @@ -33,48 +33,78 @@ # 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/", - } + default = [ + ("stream", "rad.io", "http://www.rad.io/"), + ("stream", "RadioTower", "http://www.radiotower.com/"), + ("stream", "8tracks", "http://8tracks.com/"), + ("stream", "TuneIn", "http://tunein.com/"), + ("stream", "Jango", "http://www.jango.com/"), + ("stream", "last.fm", "http://www.last.fm/"), + ("stream", "StreamFinder", "http://www.streamfinder.com/"), + ("stream", "Rhapsody (US-only)", "http://www.rhapsody.com/"), + ("stream", "Pirateradio Network", "http://www.pirateradionetwork.com/"), + ("stream", "radio-locator", "http://www.radio-locator.com/"), + ("stream", "Radio Station World", "http://radiostationworld.com/"), + ("download", "Live Music Archive(.org)", "https://archive.org/details/etree"), + ("download", "FMA, free music archive", "http://freemusicarchive.org/"), + ("download", "Audiofarm", "http://audiofarm.org/"), + ("stream", "SoundCloud", "https://soundcloud.com/"), + ("download", "ccMixter", "http://dig.ccmixter.org/"), + ("download", "mySpoonful", "http://myspoonful.com/"), + ("download", "NoiseTrade", "http://noisetrade.com/"), + ("stream", "Hype Machine", "http://hypem.com/"), + ("download", "Amazon Free MP3s", "http://www.amazon.com/b/ref=dm_hp_bb_atw?node=7933257011"), + ("stream", "Shuffler.fm", "http://shuffler.fm/"), + ("download", "ccTrax", "http://www.cctrax.com/"), + ("list", "WP: Streaming music services", "http://en.wikipedia.org/wiki/Comparison_of_on-demand_streaming_music_services"), + ("list", "WP: Music databases", "http://en.wikipedia.org/wiki/List_of_online_music_databases"), + ("commercial", "Google Play Music", "https://play.google.com/about/music/"), + ("commercial", "Deezer", "http://www.deezer.com/features/music.html"), + #("stream", "SurfMusik.de", "http://www.surfmusic.de/"), + #("stream", "MusicGOAL", "http://www.musicgoal.com/"), + ] + # 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) + if parent: + + # prepare target category + bookmarks = parent.bookmarks + if not bookmarks.streams.get(self.module): + bookmarks.streams[self.module] = [] + bookmarks.add_category(self.module) + + # fill it up later + parent.hooks["init"].append(self.populate) + def populate(self, parent): + # collect links from channel plugins for name,channel in parent.channels.items(): try: self.streams.append({ "favourite": 1, + "genre": "channel", "title": channel.title, "homepage": channel.homepage, + "type": "text/html", }) except: pass - for title,homepage in self.default.items(): + for row in self.default: + (genre, title, homepage) = row self.streams.append({ + "genre": genre, "title": title, "homepage": homepage, + "type": "text/html", }) # add to bookmarks - bookmarks.streams[self.module] = self.streams - - \ No newline at end of file + parent.bookmarks.streams[self.module] = self.streams + + # redraw category + parent.bookmarks.reload_if_current(self.module) diff -Nru streamtuner2-2.1.1/channels/live365.py streamtuner2-2.1.3/channels/live365.py --- streamtuner2-2.1.1/channels/live365.py 2014-05-26 15:53:39.000000000 +0000 +++ streamtuner2-2.1.3/channels/live365.py 2014-08-15 00:51:03.000000000 +0000 @@ -4,13 +4,21 @@ # description: Around 5000 categorized internet radio streams, some paid ad-free ones. # type: channel # category: radio -# version: 0.2 +# version: 0.3 # priority: optional # -# 2.0.9 fixed by Abhisek Sanyal +# +# +# We're currently extracting from the JavaScript; +# +# stn.set("param", "value"); +# +# And using a HTML5 player direct URL now: +# +# /cgi-bin/play.pls?stationid=%s&direct=1&file=%s.pls +# +# # - - # streamtuner2 modules @@ -19,6 +27,7 @@ import ahttp as http from channels import * from config import __print__, dbg +import action # python modules import re @@ -27,142 +36,122 @@ import gtk import copy import urllib +from itertools import groupby +from time import time +from xml.dom.minidom import parseString # channel live365 class live365(ChannelPlugin): - - # desc - module = "live365" - title = "Live365" - 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): + # desc + module = "live365" + title = "Live365" + homepage = "http://www.live365.com/" + base_url = "http://www.live365.com/" + has_search = True + listformat = "url/http" + mediatype = "audio/mpeg" + has_search = False + + # content + categories = ['Alternative', 'Blues', 'Classical', 'Country', 'Easy Listening', 'Electronic/Dance', 'Folk', 'Freeform', 'Hip-Hop/Rap', 'Inspirational', 'International', 'Jazz', 'Latin', 'Metal', 'New Age', 'Oldies', 'Pop', 'R&B/Urban', 'Reggae', 'Rock', 'Seasonal/Holiday', 'Soundtracks', 'Talk'] + current = "Alternative" + 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" - # 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) - + # 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%+]+)['"][^>]*> - ( (?:)? ) - ( \w[-\w\ /'.&]+ ) - ( (?:)? ) - """, 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) + # fixed for now + def update_categories(self): + pass + # extract stream infos + def update_streams(self, cat): - # extract stream infos - def update_streams(self, cat, search=""): + # Retrieve genere index pages + html = "" + for i in [1, 17, 33, 49]: + url = "http://www.live365.com/cgi-bin/directory.cgi?first=%i&site=web&mode=3&genre=%s&charset=UTF-8&target=content" % (i, cat.lower()) + html += http.get(url, feedback=self.parent.status) - # search / url - if (not search): - url = "http://www.live365.com/cgi-bin/directory.cgi?first=1&rows=200&mode=2&genre=" + self.cat2tag(cat) - 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""" - ['"](OK|PM_ONLY|SUBSCRIPTION).*? - href=['"](http://www.live365.com/stations/\w+)['"].*? - page['"]>([^<>]*).*? - CLASS=['"]genre-link['"][^>]*>(.+?).+? - &station_id=(\d+).+? - class=["']desc-link['"][^>]+>([^<>]*)<.*? - =["']audioQuality.+?>(\d+)\w<.+? - >DrawListenerStars\((\d+),.+? - >DrawRatingStars\((\d+),\s+(\d+),.*? - """, re.X|re.I|re.S|re.M) -# src="(http://www.live365.com/.+?/stationlogo\w+.jpg)".+? - - # append entries to result list - #__print__( dbg.DATA, html ) - ls = [] - for row in rx.findall(html): - #__print__( dbg.DATA, row ) - points = int(row[8]) - count = int(row[9]) - ls.append({ - "launch_id": row[0], - "sofo": row[0], # subscribe-or-fuck-off status flags - "state": ("" if row[0]=="OK" else gtk.STOCK_STOP), - "homepage": entity_decode(row[1]), - "title": entity_decode(row[2]), - "genre": self.strip_tags(row[3]), - "bitrate": int(row[6]), - "listeners": int(row[7]), - "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[4], - "url": self.base_url + "play/" + row[4], - "description": entity_decode(row[5]), - #"playing": row[10], - # "deleted": row[0] != "OK", - }) - return ls + # Extract from JavaScript + rx = re.compile(r""" + stn.set\( " (\w+) ", \s+ " ((?:[^"\\]+|\\.)*) "\); \s+ + """, re.X|re.I|re.S|re.M) + + # Group entries before adding them + ls = [] + for i,row in groupby(rx.findall(html), self.group_by_station): + row = dict(row) + ls.append({ + "status": (None if row["listenerAccess"] == "PUBLIC" else gtk.STOCK_STOP), + "deleted": row["status"] != "OK", + "name": row["stationName"], + "title": row["title"], + "playing": "n/a", + "id": row["id"], + "access": row["listenerAccess"], + "status": row["status"], + "mode": row["serverMode"], + "rating": int(row["rating"]), + #"rating": row["ratingCount"], + "listeners": int(row["tlh"]), + "location": row["location"], + "favicon": row["imgUrl"], + "format": self.mediatype, + "url": "%scgi-bin/play.pls?stationid=%s&direct=1&file=%s.pls" % (self.base_url, row["id"], row["stationName"]) + }) + return ls + + # itertools.groupby filter + gi = 0 + def group_by_station(self, kv): + if kv[0] == "stationName": + self.gi += 1 + return self.gi + + + # inject session id etc. into direct audio url + def UNUSED_play(self, row): + if row.get("url"): + + # params + id = row["id"] + name = row["name"] + + # get mini.cgi station resource + mini_url = "http://www.live365.com/cgi-bin/mini.cgi?version=3&templateid=xml&from=web&site=web" \ + + "&caller=&tag=web&station_name=%s&_=%i111" % (name, time()) + mini_r = http.get(mini_url, content=False) + mini_xml = parseString(mini_r.text).getElementsByTagName("LIVE365_PLAYER_WINDOW")[0] + mini = lambda name: mini_xml.getElementsByTagName(name)[0].childNodes[0].data - # 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 + # authorize with play.cgi + play_url = "" - - # html helpers - def cat2tag(self, cat): - return urllib.quote(cat.lower()) #re.sub("[^a-z]", "", - def strip_tags(self, s): - return re.sub("<.+?>", "", s) + # mk audio url + play = "http://%s/play" % mini("STREAM_URL") \ + + "?now=0&" \ + + mini("NANOCASTER_PARAMS") \ + + "&token=" + mini("TOKEN") \ + + "&AuthType=NORMAL&lid=276006-deu&SaneID=178.24.130.71-1406763621701" + + # let's see what happens + action.action.play(play, self.mediatype, self.listformat) diff -Nru streamtuner2-2.1.1/channels/modarchive.py streamtuner2-2.1.3/channels/modarchive.py --- streamtuner2-2.1.1/channels/modarchive.py 2014-05-28 15:14:25.000000000 +0000 +++ streamtuner2-2.1.3/channels/modarchive.py 2014-06-02 00:22:53.000000000 +0000 @@ -104,7 +104,7 @@ # download links from dmoz listing - def update_streams(self, cat, force=0): + def update_streams(self, cat): url = "http://modarchive.org/index.php" params = dict(query=self.catmap[cat], request="search", search_type="genre") diff -Nru streamtuner2-2.1.1/channels/musicgoal.py streamtuner2-2.1.3/channels/musicgoal.py --- streamtuner2-2.1.1/channels/musicgoal.py 2014-05-13 16:19:45.000000000 +0000 +++ streamtuner2-2.1.3/channels/musicgoal.py 2014-06-03 00:18:46.000000000 +0000 @@ -58,7 +58,7 @@ # request json API - def update_streams(self, cat, search=""): + def update_streams(self, cat): # category type: podcast or radio if cat in self.podcast: diff -Nru streamtuner2-2.1.1/channels/myoggradio.py streamtuner2-2.1.3/channels/myoggradio.py --- streamtuner2-2.1.1/channels/myoggradio.py 2014-05-13 16:53:10.000000000 +0000 +++ streamtuner2-2.1.3/channels/myoggradio.py 2014-06-30 17:04:52.000000000 +0000 @@ -23,6 +23,7 @@ from config import conf from action import action from mygtk import mygtk +import ahttp as http import re import json @@ -38,7 +39,7 @@ title = "MyOggRadio" module = "myoggradio" homepage = "http://www.myoggradio.org/" - api = "http://ehm.homelinux.org/MyOggRadio/" + api = "http://www.myoggradio.org/" listformat = "url/direct" # config data @@ -73,7 +74,7 @@ # download links from dmoz listing - def update_streams(self, cat, force=0): + def update_streams(self, cat): # result list entries = [] diff -Nru streamtuner2-2.1.1/channels/punkcast.py streamtuner2-2.1.3/channels/punkcast.py --- streamtuner2-2.1.1/channels/punkcast.py 2014-05-13 20:57:16.000000000 +0000 +++ streamtuner2-2.1.3/channels/punkcast.py 2014-06-02 00:23:22.000000000 +0000 @@ -58,7 +58,7 @@ # get list - def update_streams(self, cat, force=0): + def update_streams(self, cat): rx_link = re.compile(""" diff -Nru streamtuner2-2.1.1/channels/shoutcast.py streamtuner2-2.1.3/channels/shoutcast.py --- streamtuner2-2.1.1/channels/shoutcast.py 2014-05-28 00:50:51.000000000 +0000 +++ streamtuner2-2.1.3/channels/shoutcast.py 2014-08-05 02:39:38.000000000 +0000 @@ -5,23 +5,34 @@ # type: channel # category: radio # priority: default -# version: 1.4 +# version: 1.5 # depends: pq, re, http # 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. +# station information on shoutcast.com +# It has been aquired by Radionomy in 2014, since then significant changes +# took place. The former YP got deprecated, now seemingly undeprecated. # -# After its recent aquisition the layout got slimmed down considerably. So -# there's not a lot of information to fetch left. And this plugin is now back -# to defaulting to regex extraction instead of HTML parsing & DOM extraction. +# http://wiki.winamp.com/wiki/SHOUTcast_Radio_Directory_API +# +# But neither their Wiki nor Bulletin Board provide concrete information on +# the eligibility of open source desktop apps for an authhash. +# +# Therefore we'll be retrieving stuff from the homepage still. The new +# interface conveniently uses JSON already, so let's use that: +# +# POST http://www.shoutcast.com/Home/BrowseByGenre {genrename: Pop} +# +# We do need a catmap now too, but that's easy to aquire and will be kept +# within the cache dirs. # # # - import ahttp as http +from json import loads as json_decode import re from config import conf, __print__, dbg from pq import pq @@ -34,163 +45,95 @@ # SHOUTcast data module ---------------------------------------- class shoutcast(channels.ChannelPlugin): - # desc - api = "streamtuner2" - module = "shoutcast" - title = "SHOUTcast" - homepage = "http://www.shoutcast.com/" - base_url = "http://shoutcast.com/" - listformat = "audio/x-scpls" - - # settings - config = [ - ] - - # 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 = "" - + # desc + api = "streamtuner2" + module = "shoutcast" + title = "SHOUTcast" + homepage = "http://www.shoutcast.com/" + base_url = "http://shoutcast.com/" + listformat = "audio/x-scpls" + + # settings + config = [ + ] + + # categories + categories = [] + catmap = {"Choral": 35, "Winter": 275, "JROCK": 306, "Motown": 237, "Political": 290, "Tango": 192, "Ska": 22, "Comedy": 283, "Decades": 212, "European": 143, "Reggaeton": 189, "Islamic": 307, "Freestyle": 114, "French": 145, "Western": 53, "Dancepunk": 6, "News": 287, "Xtreme": 23, "Bollywood": 138, "Celtic": 141, "Kids": 278, "Filipino": 144, "Hanukkah": 270, "Greek": 146, "Punk": 21, "Spiritual": 211, "Industrial": 14, "Baroque": 33, "Talk": 282, "JPOP": 227, "Scanner": 291, "Mediterranean": 154, "Swing": 174, "Themes": 89, "IDM": 75, "40s": 214, "Funk": 236, "Rap": 110, "House": 74, "Educational": 285, "Caribbean": 140, "Misc": 295, "30s": 213, "Anniversary": 266, "Sports": 293, "International": 134, "Tribute": 107, "Piano": 41, "Romantic": 42, "90s": 219, "Latin": 177, "Grunge": 10, "Dubstep": 312, "Government": 286, "Country": 44, "Salsa": 191, "Hardcore": 11, "Afrikaans": 309, "Downtempo": 69, "Merengue": 187, "Psychedelic": 260, "Female": 95, "Bop": 167, "Tribal": 80, "Metal": 195, "70s": 217, "Tejano": 193, "Exotica": 55, "Anime": 277, "BlogTalk": 296, "African": 135, "Patriotic": 101, "Blues": 24, "Turntablism": 119, "Chinese": 142, "Garage": 72, "Dance": 66, "Valentine": 273, "Barbershop": 222, "Alternative": 1, "Technology": 294, "Folk": 82, "Klezmer": 152, "Samba": 315, "Turkish": 305, "Trance": 79, "Dub": 245, "Rock": 250, "Polka": 59, "Modern": 39, "Lounge": 57, "Indian": 149, "Hindi": 148, "Brazilian": 139, "Eclectic": 93, "Korean": 153, "Creole": 316, "Dancehall": 244, "Surf": 264, "Reggae": 242, "Goth": 9, "Oldies": 226, "Zouk": 162, "Environmental": 207, "Techno": 78, "Adult": 90, "Rockabilly": 262, "Wedding": 274, "Russian": 157, "Sexy": 104, "Chill": 92, "Opera": 40, "Emo": 8, "Experimental": 94, "Showtunes": 280, "Breakbeat": 65, "Jungle": 76, "Soundtracks": 276, "LoFi": 15, "Metalcore": 202, "Bachata": 178, "Kwanzaa": 272, "Banda": 179, "Americana": 46, "Classical": 32, "German": 302, "Tamil": 160, "Bluegrass": 47, "Halloween": 269, "College": 300, "Ambient": 63, "Birthday": 267, "Meditation": 210, "Electronic": 61, "50s": 215, "Chamber": 34, "Heartache": 96, "Britpop": 3, "Soca": 158, "Grindcore": 199, "Reality": 103, "00s": 303, "Symphony": 43, "Pop": 220, "Ranchera": 188, "Electro": 71, "Christmas": 268, "Christian": 123, "Progressive": 77, "Jazz": 163, "Trippy": 108, "Instrumental": 97, "Tropicalia": 194, "Fusion": 170, "Healing": 209, "Glam": 255, "80s": 218, "KPOP": 308, "Worldbeat": 161, "Mixtapes": 117, "60s": 216, "Mariachi": 186, "Soul": 240, "Cumbia": 181, "Inspirational": 122, "Impressionist": 38, "Gospel": 129, "Disco": 68, "Arabic": 136, "Idols": 225, "Ragga": 247, "Demo": 67, "LGBT": 98, "Honeymoon": 271, "Japanese": 150, "Community": 284, "Weather": 317, "Asian": 137, "Hebrew": 151, "Flamenco": 314, "Shuffle": 105} + current = "" + default = "Alternative" + empty = "" + + # redefine + streams = {} + - # redefine - streams = {} + # Extracts the category list from www.shoutcast.com, + # stores a catmap (title => id) + def update_categories(self): + html = http.get(self.base_url) + #__print__( dbg.DATA, html ) + self.categories = [] - - # 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 = [] - __print__( dbg.DATA, html ) - - #

Radio Genres

- rx = re.compile(r'[\w\s]+', re.S) - sub = [] - for uu in rx.findall(html): - __print__( dbg.DATA, uu ) - (main,name,id) = uu - name = urllib.unquote(name) - - # main category - if main: - if sub: - self.categories.append(sub) - sub = [] - self.categories.append(name) - else: - sub.append(name) - - # it's done - __print__( dbg.PROC, self.categories ) - conf.save("cache/categories_shoutcast", self.categories) - pass - - - # downloads stream list from shoutcast for given category - def update_streams(self, cat, search=""): - - if (not cat or cat == self.empty): - __print__( dbg.ERR, "nocat" ) - return [] - - #/radiolist.cfm?action=sub&string=&cat=Oldies&_cf_containerId=radiolist&_cf_nodebug=true&_cf_nocache=true&_cf_rc=0 - #/radiolist.cfm?start=19&action=sub&string=&cat=Oldies&amount=18&order=listeners - # page - url = "http://www.shoutcast.com/radiolist.cfm" - params = { - "action": "sub", - "string": "", - "cat": cat, - "order": "listeners", - "amount": conf.max_streams, - } - referer = "http://www.shoutcast.com/?action=sub&cat="+cat - html = http.get(url, params=params, referer=referer, ajax=1) - self.parent.status(0.75) - - #__print__(dbg.DATA, html) - #__print__(re.compile("id=(\d+)").findall(html)); - # new html - """ - - Play - Schlagerhoelle - das Paradies fr Schlager und Discofox - Oldies - 955 - 128 - MP3 - - """ - - # With the new shallow lists it doesn't make much sense to use - # the pyquery DOM traversal. There aren't any sensible selectors to - # extract values; it's just counting the tags. - # - # And there's a bug in PyQuery 1.2.4 and CssSelector. So make two - # attempts, alternate between regex and DOM; user preference first. - # - for use_rx in [not conf.pyquery or not pq, conf.pyquery]: - try: - entries = (self.with_regex(html) if use_rx else self.with_dom(html)) - if len(entries): - break - except Exception as e: - __print__(dbg.ERR, e) - continue - return entries - - - # Extract using regex - def with_regex(self, html): - __print__(dbg.PROC, "channels.shoutcast.update_streams: regex scraping mode") - rx_stream = re.compile( - """ - ]+ href="http://yp.shoutcast.com/sbin/tunein-station.pls\? - id=(\d+)"> ([^<>]+) - \s+ ]+ >([^<>]+) - \s+ ]+ >(\d+) - \s+ ]+ >(\d+) - \s+ ]+ >(\w+) - """, - re.S|re.I|re.X - ) - # extract entries - entries = [] - for m in rx_stream.findall(html): - #__print__(m) - (id, title, genre, listeners, bitrate, fmt) = m - entries += [{ - "id": id, - "url": "http://yp.shoutcast.com/sbin/tunein-station.pls?id=" + id, - "title": self.entity_decode(title), - #"homepage": http.fix_url(homepage), - #"playing": self.entity_decode(playing), - "genre": genre, - "listeners": int(listeners), - "max": 0, #int(uu[6]), - "bitrate": int(bitrate), - "format": self.mime_fmt(fmt), - }] - return entries - - - # Iterate over DOM instead - def with_dom(self, html): - __print__(dbg.PROC, "channels.shoutcast.update_streams: attempt PyQuery/DOM traversal") - entries = [] - for div in (pq(e) for e in pq(html).find("tr")): - entries.append({ - "title": div.find("a.transition").text(), - "url": div.find("a.transition").attr("href"), - "homepage": "", - "listeners": int(div.find("td:eq(3)").text()), - "bitrate": int(div.find("td:eq(4)").text()), - "format": self.mime_fmt(div.find("td:eq(5)").text()), - "max": 0, - "genre": cat, - }) - return entries - + # Genre list in sidebar + """
  • Adult
  • """ + rx = re.compile(r"loadStationsByGenre\( '([^']+)' [,\s]* (\d+) [,\s]* (\d+) \)", re.X) + subs = rx.findall(html) + + # group + current = [] + for (title, id, main) in subs: + self.catmap[title] = int(id) + if not int(main): + self.categories.append(title) + current = [] + self.categories.append(current) + else: + current.append(title) + self.save() + + + # downloads stream list from shoutcast for given category + def update_streams(self, cat): + + if (cat not in self.catmap): + __print__( dbg.ERR, "nocat" ) + return [] + id = self.catmap[cat] + + # page + url = "http://www.shoutcast.com/Home/BrowseByGenre" + params = { "genrename": cat } + referer = None + json = http.get(url, params=params, referer=referer, post=1, ajax=1) + self.parent.status(0.75) + + # remap JSON + entries = [] + for e in json_decode(json): + entries.append({ + "id": int(e.get("ID", 0)), + "genre": str(e.get("Genre", "")), + "title": str(e.get("Name", "")), + "playing": str(e.get("CurrentTrack", "")), + "bitrate": int(e.get("Bitrate", 0)), + "listeners": int(e.get("Listeners", 0)), + "url": "http://yp.shoutcast.com/sbin/tunein-station.pls?id=%s" % e.get("ID", "0"), + "homepage": "", + "format": "audio/mpeg" + }) + + #__print__(dbg.DATA, entries) + return entries + + + # saves .streams and .catmap + def save(self): + channels.ChannelPlugin.save(self) + conf.save("cache/catmap_" + self.module, self.catmap) + + # read previous channel/stream data, if there is any + def cache(self): + channels.ChannelPlugin.cache(self) + self.catmap = conf.load("cache/catmap_" + self.module) or {} diff -Nru streamtuner2-2.1.1/channels/surfmusik.py streamtuner2-2.1.3/channels/surfmusik.py --- streamtuner2-2.1.1/channels/surfmusik.py 2014-05-28 01:02:40.000000000 +0000 +++ streamtuner2-2.1.3/channels/surfmusik.py 2014-06-02 00:23:38.000000000 +0000 @@ -2,15 +2,15 @@ # api: streamtuner2 # title: SurfMusik # description: User collection of streams categorized by region and genre. -# version: 0.1 +# version: 0.5 # type: channel # category: radio # author: gorgonz123 # source: http://forum.ubuntuusers.de/topic/streamtuner2-zwei-internet-radios-anhoeren-au/3/ # recognizes: max_streams # -# While the categories and genre names are in German, there's a vast -# collection of international stations on Surfmusik.de +# This plugin comes in German (SurfMusik) and English (SurfMusic) variations. +# It provides a vast collection of international stations and genres. # While it's not an open source project, most entries are user contributed. # # They do have a Windows client, hencewhy it's even more important for @@ -19,13 +19,12 @@ # TV stations don't seem to work mostly. And loading the webtv/ pages would # be somewhat slow (for querying the actual mms:// streams). # -# * There's also an English version //surfmusic.com/ -# So it might make sense to duplicate it with alternative category titles. +# # import re import ahttp as http -from config import conf +from config import conf, dbg, __print__ from channels import * @@ -37,40 +36,79 @@ title = "SurfMusik" module = "surfmusik" homepage = "http://www.surfmusik.de/" - - base = "http://www.surfmusik.de/" - base2 = "http://www.surfmusic.de/" listformat = "audio/x-scpls" - categories = [ - "Genres", ["50ger 50s", "Dubstep", "Latin Jazz", "Schlager", "60ger 60s", "Electronic", "Latino", "Sega", "70ger 70s", "Eurodance ", "Lounge", "Soft", "80ger 80s", "Filmmusik", "Metal", "Sport", "90ger 90s", "Flamenco", "Merengue", "Swing", "Acid", "Gay", "Mix", "Tamil", "Ambient", "Gospel", "New Age", "Tango", "Arabische Musik", "Gothic", "News", "Techno", "Afrikanische Musik", "Groove", "Nostalgie", "Gabber", "Artist Radio ", "Halloween", "Hardstyle", "Bachata", "Hip Hop", "Oldies", "Jumpstyle", "Bhangra", "Hoerspiel Radio", "Minimal", "Balladen", "House", "Pop", "Schranz", "Big Band", "Indian", "Punk", "Top 40", "Blues", "Indisch", "Radioversprecher", "Trance", "Bollywood", "Instrumentalmusik", "Reggae", "Trip Hop", "Campusradio", "Information", "RnB", "Tropical", "Celtic", "Italo Disco ", "Rochester", "Urban", "Chillout", "Jazz", "Rock", "Variety", "Country", "Karnevalsmusik", "Rock n Roll", "Volksmusik", "Dance", "Kinderradio", "Rumba/Salsa", "Zumba", "Discofox", "Kirchlich", "Russische Chansons", "Drum n Bass", "Klassik", "Salsa"], - "Deutschland", ["Baden Wuerttemberg", "Niedersachsen", "Bayern", "Nordrhein-Westfalen", "Berlin", "Rheinland-Pfalz", "Brandenburg", "Saarland", "Bremen", "Sachsen", "Hamburg", "Sachsen-Anhalt", "Hessen", "Schleswig-Holstein", "Mecklenburg-Vorpommern", "Thueringen"], - "Europa", ["Albanien", "Griechenland", "Mallorca", "Slowakei", "Andorra", "Irland", "Malta", "Slovenien", "Armenien", "Island", "Niederlande", "Spanien", "Aserbaidschan", "Italien", "Norwegen", "Tschech. Republ", "Belgien", "Kasachstan", "Oesterreich", "Tuerkei", "Bosnien", "Kanarische Inseln", "Polen", "Ungarn", "Bulgarien", "Kirgistan", "Portugal", "Ukraine", "Daenemark", "Kroatien", "Rumaenien", "Wales", "Deutschland", "Lettland", "Russland", "Weissrussland", "England", "Liechtenstein", "Schottland", "Zypern", "Estland", "Litauen", "Schweden", "Finnland", "Luxemburg", "Schweiz", "Frankreich", "Mazedonien", "Serbien"], - "Afrika", ["Angola", "Malawi", "Aethiopien", "Mauritius", "Aegypten", "Marokko", "Algerien", "Namibia", "Benin", "Nigeria", "Burundi", "Reunion", "Elfenbeinkueste", "Senegal", "Gabun", "Simbabwe", "Ghana", "Somalia", "Kamerun", "Sudan", "Kap Verde", "Suedafrika", "Kenia", "Tansania", "Kongo", "Togo", "Libyen", "Tunesien", "Madagaskar", "Uganda", "Mali"], - "USA", ["Alabama", "Illinois", "Montana", "Rhode Island", "Alaska", "Indiana", "Nebraska", "South Carolina", "Arizona", "Iowa", "Nevada", "South Dakota", "Arkansas", "Kansas", "New Hampshire", "Tennessee", "Californien", "Kentucky", "New Jersey", "Texas", "Colorado", "Louisiana", "New Mexico", "Utah", "Connecticut", "Maine", "New York", "Vermont", "Delaware", "Maryland", "North Carolina", "Virginia", "Distr.Columbia", "Massachusetts", "North Dakota", "Washington", "Florida", "Michigan", "Ohio", "West Virginia", "Georgia", "Minnesota", "Oklahoma", "Wisconsin", "Hawaii", "Mississippi", "Oregon", "Wyoming", "Idaho", "Missouri", "Pennsylvania", "NOAA Wetter Radio"], - "Kanada", ["Alberta", "Ontario", "British Columbia", "Prince Edward Island", "Manitoba", "Québec", "Neufundland", "Saskatchewan", "New Brunswick", "Nordwest-Territorien", "Nova Scotia", "Yukon", "Nunavut",], - "Amerika", ["Mexiko", "Costa Rica", "Argentinien", "Aruba", "El Salvador", "Bolivien", "Antigua", "Guatemala", "Brasilien", "Barbados", "Honduras", "Chile", "Bahamas", "Nicaragua", "Ecuador", "Bermuda", "Panama", "Guyana", "Curaçao", "Guyana", "Domenik. Republ", "Kolumbien", "Grenada", "Paraguay", "Guadeloupe", "Uruguay", "Haiti", "Suriname", "Jamaika", "Peru", "Kaimaninseln", "Venezuela", "Kuba", "Martinique", "Puerto Rico", "St.Lucia", "Saint Martin", "Trinidad und Tobago"], - "Asien", ["Afghanistan", "Kirgistan", "Vereinigte Arabische Emirate", "Sued-Korea", "Bahrain", "Kuwait", "Bangladesch", "Libanon", "Brunei", "Malaysia", "China", "Nepal", "Guam", "Oman", "Hong Kong", "Pakistan", "Iran", "Palaestina", "Indien", "Philippinen", "Indonesien", "Saudi Arabien", "Israel", "Singapur", "Jordanien", "Sri Lanka", "Japan", "Syrien", "Kambodscha", "Taiwan", "Kasachstan", "Thailand",], - "Ozeanien", ["Australien", "Neuseeland", "Suedpol", "Fidschi", "Papanew", "Tahiti",], - #"SurfTV", - "MusikTV", "NewsTV", - "Poli", "Flug", - ] + lang = "DE" # last configured categories + base = { + "DE": ("http://www.surfmusik.de/", "genre/", "land/"), + "EN": ("http://www.surfmusic.de/", "format/", "country/"), + } + + categories = [] titles = dict( genre="Genre", title="Station", playing="Location", bitrate=False, listeners=False ) config = [ - #{"name": "surfmusik_lang", "type": "select", "select":"DE=SurfMusik.de|EN=SurfMusic.de", "description": "You can alternatively use the German or English category titles.", "category": "language"} + { + "name": "surfmusik_lang", + "value": "EN", + "type": "select", + "select":"DE=German|EN=English", + "description": "Switching to a new category title language requires reloading the category tree.", + "category": "language", + } ] + + + # Set channel title + def __init__(self, parent=None): + self.title = ("SurfMusik", "SurfMusic")[conf.get("surfmusik_lang", "EN") == "EN"] + ChannelPlugin.__init__(self, parent) # just a static list for now def update_categories(self): - pass + + lang = conf.surfmusik_lang + (base_url, path_genre, path_country) = self.base[lang] + + cats = { + "DE": ["Genres", "Deutschland", "Europa", "USA", "Kanada", "Amerika", "Afrika", "Asien", "Ozeanien", "MusicTV", "NewsTV", "Poli", "Flug"], + "EN": ["Genres", "Europe", "Germany", "USA", "Canada", "America", "Africa", "Asia", "Oceania", "MusicTV", "NewsTV", "Poli", "Flug"], + } + map = { + "Genres": "genres.htm", + "Europe": "euro.htm", "Europa": "euro.htm", + "Germany": "bundesland.htm", "Deutschland": "bundesland.htm", + "Africa": "africa.htm", "Afrika": "africa.htm", + "America": "amerika.htm", "Amerika": "amerika.htm", + "Asia": "asien.htm", "Asien": "asien.htm", + "Oceania": "ozean.htm", "Ozeanien": "ozean.htm", + "Canada": "canadian-radio-stations.htm", "Kanada": "kanada-online-radio.htm", + "USA": "staaten.htm", + } + rx_links = re.compile(r""" + ]+ \b href=" + (?:(?:https?:)?//www.surfmusi[kc].de)? /? + (?:land|country|genre|format)/ + ([\-+\w\d\s%]+) \.html" + """, re.X) + + r = [] + # Add main categories, and fetch subentries (genres or country names) + for cat in cats[lang]: + r.append(cat) + if map.get(cat): + subcats = rx_links.findall( http.get(base_url + map[cat]) ) + subcats = [x.replace("+", " ").title() for x in subcats] + r.append(sorted(subcats)) + + self.categories = r # summarize links from surfmusik - def update_streams(self, cat, force=0): + def update_streams(self, cat): + (base_url, path_genre, path_country) = self.base[conf.surfmusik_lang] entries = [] i = 0 max = int(conf.max_streams) @@ -79,19 +117,23 @@ # placeholder category if cat in ["Genres"]: path = None + # separate + elif cat in ["Poli", "Flug"]: + path = "" # tv elif cat in ["SurfTV", "MusikTV", "NewsTV"]: path = "" is_tv = 1 # genre - elif cat in self.categories[1]: - path = "genre/" + elif cat in self.categories[self.categories.index("Genres") + 1]: + path = path_genre # country else: - path = "land/" + path = path_country if path is not None: - html = http.get(self.base + path + cat.lower() + ".html") + ucat = cat.replace(" ", "+").lower() + html = http.get(base_url + path + ucat + ".html") html = re.sub("&#x?\d+;", "", html) rx_radio = re.compile(r""" @@ -106,7 +148,7 @@ # per-country list for uu in rx_radio.findall(html): - (url, homepage, name, genre, stadt) = uu + (url, homepage, name, genre, city) = uu # find mms:// for webtv stations if is_tv: @@ -121,7 +163,7 @@ "title": name, "homepage": homepage, "url": url, - "playing": stadt, + "playing": city, "genre": genre, "format": ("video/html" if is_tv else "audio/mpeg"), }) diff -Nru streamtuner2-2.1.1/channels/xiph.py streamtuner2-2.1.3/channels/xiph.py --- streamtuner2-2.1.1/channels/xiph.py 2014-05-26 15:51:40.000000000 +0000 +++ streamtuner2-2.1.3/channels/xiph.py 2014-08-12 18:36:36.000000000 +0000 @@ -86,7 +86,7 @@ # downloads stream list from xiph.org for given category - def update_streams(self, cat, search=""): + def update_streams(self, cat, search=None): # With the new JSON cache API on I-O, we can load categories individually: params = {} @@ -103,21 +103,22 @@ l = [] __print__( dbg.PROC, "processing api.dir.xiph.org JSON (via api.include-once.org cache)" ) data = json.loads(data) - for e in data.values(): + for e in data: #__print__(dbg.DATA, e) bitrate = int(e["bitrate"]) if conf.xiph_min_bitrate and bitrate and bitrate >= int(conf.xiph_min_bitrate): - l.append({ - "title": e["stream_name"], - "url": e["listen_url"], - "format": e["type"], - "bitrate": int(e["bitrate"]), - "genre": e["genre"], - "playing": e["current_song"], - "listeners": 0, - "max": 0, - "homepage": (e["homepage"] if ("homepage" in e) else ""), - }) + if not len(l) or l[-1]["title"] != e["stream_name"]: + l.append({ + "title": e["stream_name"], + "url": e["listen_url"], + "format": e["type"], + "bitrate": bitrate, + "genre": e["genre"], + "playing": e["current_song"], + "listeners": 0, + "max": 0, + "homepage": (e["homepage"] if ("homepage" in e) else ""), + }) # send back the list return l diff -Nru streamtuner2-2.1.1/channels/youtube.py streamtuner2-2.1.3/channels/youtube.py --- streamtuner2-2.1.1/channels/youtube.py 2014-05-26 15:52:47.000000000 +0000 +++ streamtuner2-2.1.3/channels/youtube.py 2014-07-05 18:31:36.000000000 +0000 @@ -3,11 +3,10 @@ # title: Youtube # description: Channel, playlist and video browsing for youtube. # type: channel -# version: 0.1 +# version: 0.2 # category: video # priority: optional # suggests: youtube-dl -# requires: ahttp # # # Lists recently popular youtube videos by category or channels. @@ -63,6 +62,7 @@ module = "youtube" homepage = "http://www.youtube.com/" listformat = "url/youtube" + has_search = True fmt = "video/youtube" titles = dict( genre="Channel", title="Title", playing="Playlist", bitrate=False, listeners=False ) @@ -87,7 +87,7 @@ categories = [ "mostPopular", - ["Music", "Comedy", "Movies", "Shows", "Short Movies", "Trailers", "Film & Animation", "Entertainment", "News & Politics"], + ["Music", "Comedy", "Movies", "Shows", "Trailers", "Film & Animation", "Entertainment", "News & Politics"], "topics", ["Pop", "Billboard charts", "Rock", "Hip Hop", "Classical", "Soundtrack", "Ambient", "Jazz", "Blues", "Soul", "Country", "Disco", "Dance", "House", "Trance", "Techno", "Electronica"], @@ -112,6 +112,13 @@ "description": "Filter by region id.", "category": "auth", }, + { + "name": "youtube_wadsworth", + "type": "boolean", + "value": 0, + "description": "Apply Wadsworth constant.", + "category": "filter", + }, ] # from GET https://www.googleapis.com/youtube/v3/videoCategories?part=id%2Csnippet& @@ -177,13 +184,18 @@ # retrieve and parse - def update_streams(self, cat, force=0, search=None): + def update_streams(self, cat, search=None): entries = [] channels = self.categories[self.categories.index("my channels") + 1] + # plain search request for videos + if search is not None: + for row in self.api("search", type="video", regionCode=conf.youtube_region, q=search): + entries.append( self.wrap3(row, {"genre": ""}) ) + # Most Popular - if cat == "mostPopular": + elif cat == "mostPopular": #for row in self.api("feeds/api/standardfeeds/%s/most_popular"%conf.youtube_region, ver=2): # entries.append(self.wrap2(row)) for row in self.api("videos", chart="mostPopular", regionCode=conf.youtube_region): @@ -217,10 +229,6 @@ self.update_streams_partially_done(entries) self.parent.status(i / 15.0) - # plain search request for videos - elif search is not None: - for row in self.api("search", type="video", regionCode=conf.youtube_region, q=search): - entries.append( self.wrap3(row, {"genre": ""}) ) # empty entries else: @@ -301,7 +309,7 @@ data.update(dict( url = "http://youtube.com/v/" + id, - homepage = "https://youtube.com/watch?v=" + id, + homepage = "http://youtu.be/" + id + ("?wadsworth=1" if conf.youtube_wadsworth else ""), format = self.fmt, title = row["snippet"]["title"], )) diff -Nru streamtuner2-2.1.1/config.py streamtuner2-2.1.3/config.py --- streamtuner2-2.1.1/config.py 2014-05-28 15:09:19.000000000 +0000 +++ streamtuner2-2.1.3/config.py 2014-08-05 19:52:00.000000000 +0000 @@ -214,15 +214,15 @@ # error colorization dbg = type('obj', (object,), { - "ERR": "[ERR]", # red ERROR - "INIT": "[INIT]", # red INIT ERROR - "PROC": "[PROC]", # green PROCESS - "CONF": "[CONF]", # brown CONFIG DATA - "UI": "[UI]", # blue USER INTERFACE BEHAVIOUR - "HTTP": "[HTTP]", # magenta HTTP REQUEST - "DATA": "[DATA]", # cyan DATA - "INFO": "[INFO]", # gray INFO - "STAT": "[STATE]", # gray CONFIG STATE + "ERR": r"[ERR]", # red ERROR + "INIT": r"[INIT]", # red INIT ERROR + "PROC": r"[PROC]", # green PROCESS + "CONF": r"[CONF]", # brown CONFIG DATA + "UI": r"[UI]", # blue USER INTERFACE BEHAVIOUR + "HTTP": r"[HTTP]", # magenta HTTP REQUEST + "DATA": r"[DATA]", # cyan DATA + "INFO": r"[INFO]", # gray INFO + "STAT": r"[STATE]", # gray CONFIG STATE }) diff -Nru streamtuner2-2.1.1/debian/changelog streamtuner2-2.1.3/debian/changelog --- streamtuner2-2.1.1/debian/changelog 2014-06-06 06:16:52.000000000 +0000 +++ streamtuner2-2.1.3/debian/changelog 2014-08-17 15:02:13.000000000 +0000 @@ -1,3 +1,15 @@ +streamtuner2 (2.1.3-1) unstable; urgency=medium + + * Imported Upstream version 2.1.3 + + -- TANIGUCHI Takaki Mon, 18 Aug 2014 00:02:02 +0900 + +streamtuner2 (2.1.2-1) unstable; urgency=medium + + * Imported Upstream version 2.1.2 + + -- TANIGUCHI Takaki Sat, 09 Aug 2014 19:48:15 +0900 + streamtuner2 (2.1.1-1) unstable; urgency=medium * Imported Upstream version 2.1.1 diff -Nru streamtuner2-2.1.1/g.py streamtuner2-2.1.3/g.py --- streamtuner2-2.1.1/g.py 1970-01-01 00:00:00.000000000 +0000 +++ streamtuner2-2.1.3/g.py 2014-07-31 02:45:40.000000000 +0000 @@ -0,0 +1,15 @@ +from itertools import groupby + +ls = [(0,7), (1,7), (0,5), (1,5), (2,5), (0,9), (9,9)] + +i = 0 +def grp(kv): + global i + if kv[0] == 0: + i += 1 + return i + + +for i,row in groupby(ls, grp): + print list(row) + diff -Nru streamtuner2-2.1.1/gtk2.xml streamtuner2-2.1.3/gtk2.xml --- streamtuner2-2.1.1/gtk2.xml 2014-05-28 14:13:18.000000000 +0000 +++ streamtuner2-2.1.3/gtk2.xml 2014-08-15 00:44:08.000000000 +0000 @@ -1,6 +1,6 @@ - + @@ -47,13 +47,10 @@ True True True - True True True - automatic - automatic True @@ -82,8 +79,6 @@ True True - automatic - automatic 540 @@ -264,8 +259,6 @@ True True - automatic - automatic True @@ -463,7 +456,6 @@ 5 500 out - True False False True @@ -616,7 +608,6 @@ True True - True gtk-save-as False False @@ -686,7 +677,6 @@ False 20 - True gtk-home False False @@ -804,7 +794,6 @@ True True - True False False True @@ -1069,1799 +1058,143 @@ - - False - 5 - station search - center-on-parent - dialog - False - center - 0.95999999999999996 - - - - - True - False - 2 - - - True - False - end - - - cancel - True - True - True - - - - False - False - 0 - - - - - True - True - - - False - False - 1 - - - - - google it - True - True - True - Instead of searching in the station list, just look up the above search term on google. - half - - - - False - False - 2 - - - - - query srv - True - False - True - Instead of doing a cache search, go through the search functions on the directory service homepages. (UNIMPLEMENTED) - half - - - - False - False - 3 - - - - - cache _search - True - False - True - True - True - Start searching for above search term in the currently loaded station lists. Doesn't find *new* information, just looks through the known data. - True - - - - - False - False - 4 - - - - - False - False - end - 0 - - - - - True - False - 20 - - - True - False - <b><big>search</big></b> - True - - - True - True - 0 - - - - - True - False - Which channels/directories to look through. - 4 - 4 - 5 - 1 - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all channels - True - True - False - True - True - - - - - True - True - 1 - - - - - True - False - - - True - False - for - - - True - True - 0 - - - - - True - True - True - True - A single word to search for in all stations. - - True - False - False - True - True - - - True - True - 1 - - - - - True - False - - - True - True - 2 - - - - - True - True - 2 - - - - - True - False - In which fields to look for the search term. - 3 - True - - - - True - True - True - none - - - False - False - 0 - - - - - in title - True - True - False - True - True - - - True - True - 1 - - - - - in description - True - True - False - True - True - - - True - True - 2 - - - - - any fields - True - True - False - True - - - True - True - 3 - - - - - - True - True - True - none - - - False - False - 4 - - - - - True - True - 3 - - - - - True - False - In which fields to look for the search term. - 3 - True - - - - True - True - True - none - - - False - False - 0 - - - - - homepage url - True - True - False - True - True - - - True - True - 1 - - - - - extra info - True - True - False - True - - - True - True - 2 - - - - - and genre - True - True - False - True - - - True - True - 3 - - - - - - True - True - True - none - - - False - False - 4 - - - - - True - True - 4 - - - - - True - False - - - - - - - - - - - - True - True - 5 - - - - - True - True - 1 - - - - - - cancel - togglebutton1 - google_search - server_search - cache_search - - - - True - False - - - True - False - play - True - - - - - - True - False - record - True - - - - - - True - False - bookmark - True - - - - - - True - False - Extensions - True - - - - - True - False - - - - - True - False - save - True - - - - - - True - False - edit - True - - - - - - True - False - - - - - True - False - station homepage - True - - - - - - False - 5 - normal - - - - True - False - 2 - - - True - False - end - - - cancel - True - True - True - - - - False - False - 0 - - - - - ok - True - True - True - - - - False - False - 1 - - - - - False - True - end - 0 - - - - - True - False - 3 - 3 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - - Fri,Sat 20:00-21:00 - False - False - True - True - - - 1 - 2 - 1 - 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + True + False + gtk-harddisk + + + True + False + gtk-add + + + 325 + False + 5 + station search + center-on-parent + True + dialog + center + 0.94999999999999996 + + + + + False + + + False True True - 1 + 0 + + + + + + + True + False + + + True + False + play + True + + + + + + True + False + record + True + + + + + + True + False + bookmark + True + + + + + + True + False + Extensions + True + + + + + True + False + + + + + True + False + save + True + + + + + + True + False + edit + True + + + + + + True + False + + + + + True + False + station homepage + True + + + + + + False + 5 + normal + + + + + False + + + False + + + True + True + 0 - - timer_cancel - timer_ok - False @@ -2913,8 +1246,8 @@ 5 inspect/edit stream data center-on-parent + True True - False 0.94999999999999996 @@ -3494,6 +1827,56 @@ + + + + + + True + False + Channel tab position + True + + + True + False + + + True + False + Top + True + + + + + + True + False + Left + True + + + + + + True + False + Bottom + True + + + + + + True + False + Right + True + + + + diff -Nru streamtuner2-2.1.1/gtk3.xml streamtuner2-2.1.3/gtk3.xml --- streamtuner2-2.1.1/gtk3.xml 2014-05-28 14:45:03.000000000 +0000 +++ streamtuner2-2.1.3/gtk3.xml 2014-08-15 00:44:21.000000000 +0000 @@ -796,7 +796,6 @@ True True - 30 False False @@ -1059,14 +1058,25 @@ + + True + False + gtk-harddisk + + + True + False + gtk-add + + 325 False - 0.95999999999999996 + 0.94999999999999996 5 station search center-on-parent + True dialog - False center @@ -1081,80 +1091,19 @@ False end - - cancel - True - True - True - - - - False - False - 0 - + - - True - True - - - False - False - 1 - + - - google it - True - True - True - Instead of searching in the station list, just look up the above search term on google. - half - - - - False - False - 2 - + - - query srv - True - False - True - Instead of doing a cache search, go through the search functions on the directory service homepages. (UNIMPLEMENTED) - half - - - - False - False - 3 - + - - cache _search - True - False - True - True - True - Start searching for above search term in the currently loaded station lists. Doesn't find *new* information, just looks through the known data. - True - - - - - False - False - 4 - + @@ -1183,948 +1132,24 @@ - + + + + True False - Which channels/directories to look through. - 4 - 4 - 5 - 1 - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all channels - True - True - False - 0.5 - True - True - - - - - True - True - 1 - - - - - True - False - - - True - False - for - - - True - True - 0 - + + True + False + 10 + for + + + True + True + 0 + @@ -2145,142 +1170,51 @@ - - True - False - - - True - True - 2 - - - - - True - True - 2 - - - - - True - False - In which fields to look for the search term. - 3 - True - - - - True - True - True - none - - - False - False - 0 - - - - - in title - True - True - False - 0.5 - True - True - - - True - True - 1 - - - - - in description - True - True - False - 0.5 - True - True - - - True - True - 2 - - - - - any fields - True - True - False - 0.5 - True - - - True - True - 3 - - - - - + True - True - True - none + False - False - False - 4 + True + True + 2 True True - 3 + 2 - + True False - In which fields to look for the search term. - 3 - True - - + True - True - True - none + False + 10 + 10 + in - False - False + True + True 0 - - homepage url + + all channels True True False 0.5 True True + search_dialog_current True @@ -2289,13 +1223,15 @@ - - extra info + + just current True True False 0.5 + True True + search_dialog_all True @@ -2303,59 +1239,65 @@ 2 + + + True + True + 3 + + + + + + + + True + False + 20 - - and genre + + Cache _find True - True + False False - 0.5 - True + Start searching for above search term in the currently loaded station lists. Doesn't find *new* information, just looks through the known data. + image1 + half + True + + True True - 3 + 0 - - + + Server _search True - True + False + True + True + True + True True - none + Instead of doing a cache search, go through the search functions on the directory service homepages. (UNIMPLEMENTED) + image2 + True + - False - False - 4 + True + True + 1 True True - 4 - - - - - True - False - - - - - - - - - - - - True - True 5 @@ -2368,13 +1310,6 @@ - - cancel - togglebutton1 - google_search - server_search - cache_search - True @@ -2458,7 +1393,8 @@ False 5 normal - + + True @@ -2872,6 +1808,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True True @@ -2974,6 +2036,27 @@ + + + + + + + + + + + + + + + + + + + + + True @@ -3039,8 +2122,8 @@ 5 inspect/edit stream data center-on-parent + True True - False @@ -3401,8 +2484,8 @@ bookmark True - + @@ -3413,8 +2496,8 @@ True True - + @@ -3425,9 +2508,9 @@ True True - - + + @@ -3500,8 +2583,8 @@ True True - + @@ -3598,6 +2681,56 @@ + + + + + + True + False + Channel tab position + True + + + True + False + + + True + False + Top + True + + + + + + True + False + Left + True + + + + + + True + False + Bottom + True + + + + + + True + False + Right + True + + + + diff -Nru streamtuner2-2.1.1/help/search.page streamtuner2-2.1.3/help/search.page --- streamtuner2-2.1.1/help/search.page 2014-05-28 12:57:59.000000000 +0000 +++ streamtuner2-2.1.3/help/search.page 2014-06-02 10:28:28.000000000 +0000 @@ -25,24 +25,25 @@

    You can get to the search dialog via Edit Find or Ctrl+F. Centrally to this dialog is the text field, where you can specify the phrase to scan for.

    -

    Above you can check which channel plugins to inspect for the search term. Using this + -

    Lastly, there are three search methods. You mostly want to use the cache search, - which just scans through the station lists streamtuner2 has downloaded. Since you are mostly - looking for something you had already seen, this will give you the desired results.

    - -

    The server search would try to do a live search on the directory servers, - providing you with the most recent data. However, it's not implemented for all channel - plugins, and therefore brings limited output.

    - -

    Use the button google it as last resort, if streamtuner2 didn't find anything.

    +

    And then there are two search methods. You mostly want to use + the live Server search. It passes your search terms to + the actual directory services, and loads the most recent data into a + result list (this might take a few seconds). It's not implemented + for all channel plugins however.

    + +

    With Cache find would just look up entries in your + already downloaded channel/genre lists. Since often you are just + looking for something you had already seen, this will give you the + desired results.

    diff -Nru streamtuner2-2.1.1/mygtk.py streamtuner2-2.1.3/mygtk.py --- streamtuner2-2.1.1/mygtk.py 2014-05-28 01:28:11.000000000 +0000 +++ streamtuner2-2.1.3/mygtk.py 2014-08-15 00:32:53.000000000 +0000 @@ -134,8 +134,8 @@ col.add_attribute(rend, attr, val) # next datapos += 1 + #__print__(dbg.INFO, cell, len(cell)) - __print__(dbg.INFO, cell, len(cell)) # add column to treeview widget.append_column(col) # finalize widget @@ -220,7 +220,7 @@ # list types ls = gtk.TreeStore(str, str) - print(entries) + #__print__(dbg.DATA, ".tree", entries) # add entries for entry in entries: @@ -301,9 +301,12 @@ # gtk.Notebook if t == gtk.Notebook: r[wn]["page"] = w.get_current_page() + r[wn]["tab_pos"] = int(w.get_tab_pos()) #print(r) return r + gtk_position_type_enum = [gtk.POS_LEFT, gtk.POS_RIGHT, gtk.POS_TOP, gtk.POS_BOTTOM] + #-- restore window and widget properties # @@ -341,6 +344,8 @@ # gtk.Notebook if method == "page": w.set_current_page(args) + if method == "tab_pos": + w.set_tab_pos(r[wn]["tab_pos"]) pass diff -Nru streamtuner2-2.1.1/_package.epm streamtuner2-2.1.3/_package.epm --- streamtuner2-2.1.1/_package.epm 2014-05-28 16:49:16.000000000 +0000 +++ streamtuner2-2.1.3/_package.epm 2014-08-15 01:05:56.000000000 +0000 @@ -1,5 +1,5 @@ %product streamtuner2 - internet radio browser -%version 2.1.1 +%version 2.1.3 %vendor Mario Salzer %license %copyright Placed into the Public Domain, 2009-2014 @@ -33,7 +33,7 @@ f 644 root root /usr/share/pixmaps/streamtuner2.png ./logo.png f 644 root root /usr/share/streamtuner2/gtk2.xml ./gtk2.xml f 644 root root /usr/share/streamtuner2/gtk3.xml ./gtk3.xml -#f 644 root root /usr/share/streamtuner2/pson.py ./pson.py +#f 644 root root /usr/share/streamtuner2/pson.py ./pson.py #f 644 root root /usr/share/streamtuner2/processing.py ./processing.py f 644 root root /usr/share/streamtuner2/compat2and3.py ./compat2and3.py f 644 root root /usr/share/streamtuner2/action.py ./action.py @@ -47,8 +47,14 @@ d 755 root root /usr/share/streamtuner2/channels - f 644 root root /usr/share/streamtuner2/channels/__init__.py ./channels/__init__.py f 644 root root /usr/share/streamtuner2/channels/_generic.py ./channels/_generic.py +f 644 root root /usr/share/streamtuner2/channels/dirble.py ./channels/dirble.py +f 644 root root /usr/share/streamtuner2/channels/dirble.png ./channels/dirble.png +f 644 root root /usr/share/streamtuner2/channels/icast.py ./channels/icast.py +f 644 root root /usr/share/streamtuner2/channels/icast.png ./channels/icast.png f 644 root root /usr/share/streamtuner2/channels/internet_radio.py ./channels/internet_radio.py f 644 root root /usr/share/streamtuner2/channels/internet_radio.png ./channels/internet_radio.png +f 644 root root /usr/share/streamtuner2/channels/itunes.py ./channels/itunes.py +f 644 root root /usr/share/streamtuner2/channels/itunes.png ./channels/itunes.png f 644 root root /usr/share/streamtuner2/channels/jamendo.py ./channels/jamendo.py f 644 root root /usr/share/streamtuner2/channels/jamendo.png ./channels/jamendo.png f 644 root root /usr/share/streamtuner2/channels/live365.py ./channels/live365.py @@ -75,8 +81,6 @@ #-- scripts #d 755 root root /usr/share/streamtuner2/scripts - #f 644 root root /usr/share/streamtuner2/scripts/radiotop40_de.py ./scripts/radiotop40_de.py -#-- themes -#f 644 root root /usr/share/streamtuner2/themes/MountainDew/gtk-2.0/gtkrc ./themes/MountainDew/gtk-2.0/gtkrc #-- help files f 644 root root /usr/share/man/man1/streamtuner2.1 ./help/streamtuner2.1 d 755 root root /usr/share/doc/streamtuner2/help - @@ -90,6 +94,8 @@ f 644 root root /usr/share/doc/streamtuner2/help/channel_myoggradio.page ./help/channel_myoggradio.page f 644 root root /usr/share/doc/streamtuner2/help/channel_shoutcast.page ./help/channel_shoutcast.page f 644 root root /usr/share/doc/streamtuner2/help/channel_xiph.page ./help/channel_xiph.page +f 644 root root /usr/share/doc/streamtuner2/help/channel_youtube.page ./help/channel_youtube.page +f 644 root root /usr/share/doc/streamtuner2/help/channel_surfmusik.page ./help/channel_surfmusik.page f 644 root root /usr/share/doc/streamtuner2/help/channels.page ./help/channels.page f 644 root root /usr/share/doc/streamtuner2/help/cli.page ./help/cli.page f 644 root root /usr/share/doc/streamtuner2/help/config_apps.page ./help/config_apps.page diff -Nru streamtuner2-2.1.1/README streamtuner2-2.1.3/README --- streamtuner2-2.1.1/README 2014-05-28 14:07:06.000000000 +0000 +++ streamtuner2-2.1.3/README 2014-08-15 01:02:39.000000000 +0000 @@ -18,68 +18,23 @@ sudo apt-get install python python-gtk2 python-glade2 python-xdg +The gtk*.xml file represents the GUI. So with glade installed, you +can inspect and adapt the interface. + + +alternatives +------------ + +* http://sourceforge.net/projects/radiotray/ +* https://sites.google.com/site/glrpgreatlittleradioplayer/ +* http://tunapie.sourceforge.net/ +* VLC also has a few directory discovery services built-in +* Rythmbox comes with last.fm, libre.fm, radio lookups -The *.glade file represents the GUI. So if you have the glade-3 -application installed, you can inspect and enhance the interface. -Give it a try, report back. - - -development state ------------------ - -There was a lengthy development pause, actual maintenance time of -this application is probably 2-3 months now. However, it is mostly -feature-complete meanwhile. The internal application structures are -somewhat settled. Some modules are still pretty rough (json format, -http functions), and especially the action module for playing and -recording isn't very autonomic (though has some heuristic and uses -stream format hints). - -The directory modules mostly work. If they don't, it's just a -matter of adapting the regular expressions. Shoutcast and Live365 -lately changed the HTML output, so broke. Therefore PyQuery parsing -methods were implemented, which extract stream info from HTML soup -using CSS/jQuery selectors. -And it should be pretty easy to add new ones. There will also -somewhen be a feature to add simple station 'scripts'; in testing. - - -comparison to streamtuner1 --------------------------- - -Streamtuner1 has been written around 2002-2004. At this time it was -totally unfeasible to write any responsive application in a scripting -language. Therefore it was implemented in C, which made it speedy. - -Using C however has some drawbacks. The codebase is more elaborate, -and it often takes more time to adapt things. - -Personally I had some occasional crashes because of corrupt data -from some of the directory services. Because that was more difficult -to fix in C code, this rewrite started. It's purely for practical -purposes, not because there was anything wrong with streamtuner1. - -streamtuner2 being implemented directly in Python (the C version had -a Python plugin), cuts down the code size considerably. It's much -easier to add new plugins. (Even though it doesn't look that way yet.) - -For older machines or netbooks, the C streamtuner1 might overall -remain the better choice, btw. Running inside the Python VM makes this -variant more stable, but also way more memory hungry. (This has been -reduced with some de-optimizations already.) - - -advertisement -------------- - -If you are looking for a lightweight alternative, Tunapie is still -in development, and it has a working Shoutcast+Xiph reader. -http://tunapie.sourceforge.net/ Streamtuner2 CLI can also be used as proxy server for streamtuner1. There is a wrapper available. But there was nobody yet to be found who -wanted it set up globally. (Else streamtuner1 would just require a patch -in /etc/hosts to work again.) +wanted it set up globally. It's available as st1proxy.tgz and cli-mode-only.tgz from http://milki.include-once.org/streamtuner2/ext/ Contact me for setup help. Requires a webserver with unrestrained @@ -96,7 +51,29 @@ history ------- -2.1.1 +2.1.3 (2014-08-15) +- New plugin for Dirble.com introduced. +- Channel tabs can now be rearranged from notebook top to left side. +- Live365 was fixed again. +- Xiph cache service was fixed, and duplicates are now filtered out. +- Category map storage is now handled by backend instead of channels. +- Shorter Youtube homepage URLs are used, HTTP headers compacted. + + +2.1.2 (2014-07-31) +- Listing from the renewed Radionomy Shoutcast has been fixed. +- Live365 was disabled. +- New iTunes Radio stations channel (via RoliSoft Radio Playlist API). +- New channel module "iCast.io" as seen in VLC. +- SurfMusic.de is now available in a localized English display. +- Shorter Youtube URLs are now used, Wadsworth constant available. +- MyOggRadio.org API interaction fixed. +- Fixed cache search to copy results before overwriting category. +- Slim new search dialog offers scanning all channels or just current. +- More online music service links have been added. +- Better post-extraction cleanup. + +2.1.1 (2014-05-28) - Added SurfMusik and Youtube plugin channels. Google/DMOZ removed. - Jamendo viewing now utilizes the v3.0 JSON API for genres/tracks and uses cover images instead of favicons. @@ -112,7 +89,7 @@ - A history plugin was added. And extension hooks{} support started. - Some more Python3 fixes applied. Documentation was adapted. -2.1.0 +2.1.0 (2014-01-05) - support for running on Python3 or Python2, as well as Gtk3 (PyGI) and Gtk2 (PyGtk with Python2) bindings - fixed Shoutcast, DMOZ, Live365 diff -Nru streamtuner2-2.1.1/st2.py streamtuner2-2.1.3/st2.py --- streamtuner2-2.1.1/st2.py 2014-05-28 16:49:16.000000000 +0000 +++ streamtuner2-2.1.3/st2.py 2014-08-15 01:05:56.000000000 +0000 @@ -5,10 +5,10 @@ # title: streamtuner2 # description: Directory browser for internet radio / audio streams # depends: pygtk | pygi, threading, pyquery, kronos, requests -# version: 2.1.1 +# version: 2.1.3 # author: mario salzer # license: public domain -# url: http://freshmeat.net/projects/streamtuner2 +# url: http://freshcode.club/projects/streamtuner2 # config: # category: multimedia # @@ -68,6 +68,8 @@ import sys import os, os.path import re +from collections import namedtuple +from copy import copy # threading or processing module try: @@ -94,7 +96,7 @@ import favicon -__version__ = "2.1.1" +__version__ = "2.1.3" # this represents the main window @@ -111,6 +113,9 @@ add_signals = {} # channel gtk-handler signals hooks = { "play": [favicon.download_playing], # observers queue here + "init": [], + "config_load": [], + "config_save": [], } # status variables @@ -153,8 +158,12 @@ except: pass # fails for disabled/reordered plugin channels - # display current open channel/notebook tab + # late plugin initializations gui_startup(17/20.0) + [callback(self) for callback in self.hooks["init"]] + + # display current open channel/notebook tab + gui_startup(18/20.0) self.current_channel = self.current_channel_gtk() try: self.channel().first_show() except: __print__(dbg.INIT, "main.__init__: current_channel.first_show() initialization error") @@ -184,6 +193,10 @@ "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)), + "menu_notebook_pos_top": lambda w: self.notebook_channels.set_tab_pos(2), + "menu_notebook_pos_left": lambda w: self.notebook_channels.set_tab_pos(0), + "menu_notebook_pos_right": lambda w: self.notebook_channels.set_tab_pos(1), + "menu_notebook_pos_bottom": lambda w: self.notebook_channels.set_tab_pos(3), # win_config "menu_properties": config_dialog.open, "config_cancel": config_dialog.hide, @@ -209,9 +222,8 @@ # search dialog "quicksearch_set": search.quicksearch_set, "search_open": search.menu_search, - "search_go": search.start, - "search_srv": search.start, - "search_google": search.google, + "search_go": search.cache_search, + "search_srv": search.server_search, "search_cancel": search.cancel, "true": lambda w,*args: True, # win_streamedit @@ -222,12 +234,10 @@ }.items() ) + list( self.add_signals.items() ) )) # actually display main window - gui_startup(99/100.0) + gui_startup(98.9/100.0) self.win_streamtuner2.show() - - #-- Shortcut for glade.get_widget() # Allows access to widgets as direct attributes instead of using .get_widget() # Also looks in self.channels[] for the named channel plugins @@ -237,29 +247,24 @@ else: return self.get_object(name) # or gives an error if neither exists - - # custom-named widgets are available from .widgets{} not via .get_widget() + # Custom-named widgets are available from .widgets{} not via .get_widget() def get_widget(self, name): if name in self.widgets: return self.widgets[name] else: return gtk.Builder.get_object(self, name) - - - - # returns the currently selected directory/channel object + # returns the currently selected directory/channel object (remembered position) def channel(self): return self.channels[self.current_channel] - + # returns the currently selected directory/channel object (from gtk) def current_channel_gtk(self): i = self.notebook_channels.get_current_page() try: return self.channel_names[i] except: return "bookmarks" - - # notebook tab clicked + # Notebook tab clicked def channel_switch(self, notebook, page, page_num=0, *args): # can be called from channelmenu as well: @@ -273,19 +278,16 @@ # if first selected, load current category try: __print__(dbg.PROC, "channel_switch: try .first_show", self.channel().module); - __print__(self.channel().first_show) - __print__(self.channel().first_show()) + self.channel().first_show() except: __print__(dbg.INIT, "channel .first_show() initialization error") - - # convert ListStore iter to row number + # 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 + # Currently selected entry in stations list, return complete data dict def row(self): return self.channel().stations() [self.rowno()] @@ -293,44 +295,37 @@ # 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 + # Fetches a single varname from currently selected station entry def selected(self, name="url"): return self.row().get(name) - - - - # play button + + # Play button def on_play_clicked(self, widget, event=None, *args): row = self.row() if row: self.channel().play(row) - [hook(row) for hook in self.hooks["play"]] + [callback(row) for callback in self.hooks["play"]] - - # streamripper + # Recording: invoke streamripper for current stream URL def on_record_clicked(self, widget): row = self.row() action.record(row.get("url"), row.get("format", "audio/mpeg"), "url/direct", row=row) - - # browse stream + # Open stream homepage in web browser def on_homepage_stream_clicked(self, widget): url = self.selected("homepage") action.browser(url) - - # browse channel + # Browse to channel homepage (double click on notebook tab) def on_homepage_channel_clicked(self, widget, event=2): if event == 2 or event.type == gtk.gdk._2BUTTON_PRESS: __print__(dbg.UI, "dblclick") action.browser(self.channel().homepage) - - # reload stream list in current channel-category + # Reload stream list in current channel-category def on_reload_clicked(self, widget=None, reload=1): __print__(dbg.UI, "reload", reload, self.current_channel, self.channels[self.current_channel], self.channel().current) category = self.channel().current @@ -338,30 +333,26 @@ 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) + # 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 + # Stop reload/update threads def on_stop_clicked(self, widget): while self.working: thread = self.working.pop() thread.stop() - - # click in category list + # Click in category list def on_category_clicked(self, widget, event, *more): category = self.channel().currentcat() __print__(dbg.UI, "on_category_clicked", category, self.current_channel) self.on_reload_clicked(None, reload=0) pass - - # add current selection to bookmark store + # 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) @@ -373,19 +364,16 @@ # refresh bookmarks tab self.bookmarks.load(self.bookmarks.default) - - # reload category tree + # 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 + # 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 + # Save stream to file (.m3u) def save_as(self, widget): row = self.row() default_fn = row["title"] + ".m3u" @@ -394,23 +382,24 @@ action.save(row, fn) pass - - # save current stream URL into clipboard + # Save current stream URL into clipboard def menu_copy(self, w): gtk.clipboard_get().set_text(self.selected("url")) - - # remove an entry + # Remove a stream entry def delete_entry(self, w): n = self.rowno() del self.channel().stations()[ n ] self.channel().switch() self.channel().save() - - # stream right click + # Richt clicking a stream opens an action content menu def station_context_menu(self, treeview, event): return station_context_menu(treeview, event) # wrapper to the static function + + # Alternative Notebook channel tabs between TOP and LEFT position + def switch_notebook_tabs_position(self, w, pos): + self.notebook_channels.set_tab_pos(pos); @@ -577,10 +566,15 @@ # and also: quick search textbox (uses main.q instead) class search (auxiliary_window): + # either current channel, or last channel (avoid searching in bookmarks) + current = None # show search dialog def menu_search(self, w): self.search_dialog.show(); + if not self.current or main.current_channel != "bookmarks": + self.current = main.current_channel + self.search_dialog_current.set_label("just %s" % main.channels[self.current].title) # hide dialog box again @@ -588,46 +582,41 @@ self.search_dialog.hide() return True # stop any other gtk handlers - - # perform search - def start(self, *w): + + # prepare variables + def prepare_search(self): + main.status("Searching... Stand back.") self.cancel() - - # prepare variables self.q = self.search_full.get_text().lower() - entries = [] + if self.search_dialog_all.get_active(): + self.targets = main.channels.keys() + else: + self.targets = [self.current] main.bookmarks.streams["search"] = [] + # perform search + def cache_search(self, *w): + self.prepare_search() + entries = [] # 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 - + fields = ["title", "playing", "homepage"] + for i,cn in enumerate([main.channels[c] for c in self.targets]): + if cn.streams: # skip disabled plugins # categories - for cat in main.channels[c].streams.keys(): - + for cat in cn.streams.keys(): # stations - for row in main.channels[c].streams[cat]: - - # assemble text fields + for row in cn.streams[cat]: + # assemble text fields to compare text = " ".join([row.get(f, " ") for f in fields]) - - # compare if text.lower().find(self.q) >= 0: - - # add result + row = copy(row) + row["genre"] = c + " " + row.get("genre", "") entries.append(row) + self.show_results(entries) - - # display "search" in "bookmarks" + # display "search" in "bookmarks" + def show_results(self, entries): + main.status(1.0) main.channel_switch(None, "bookmarks", 0) main.bookmarks.set_category("search") # insert data and show @@ -636,14 +625,21 @@ # 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()) + def server_search(self, w): + self.prepare_search() + entries = [] + for i,cn in enumerate([main.channels[c] for c in self.targets]): + if cn.has_search: # "search" in cn.update_streams.func_code.co_varnames: + __print__(dbg.PROC, "has_search:", cn.module) + try: + add = cn.update_streams(cat=None, search=self.q) + for row in add: + row["genre"] = cn.title + " " + row.get("genre", "") + entries += add + except: + continue + #main.status(main, 1.0 * i / 15) + self.show_results(entries) # search text edited in text entry box @@ -746,6 +742,7 @@ self.win_config.resize(565, 625) self.load_config(conf.__dict__, "config_") self.load_config(conf.plugins, "config_plugins_") + [callback() for callback in self.hooks["config_load"]] self.win_config.show() first_open = 1 @@ -877,7 +874,8 @@ def save(self, widget): self.save_config(conf.__dict__, "config_") self.save_config(conf.plugins, "config_plugins_") - self.apply_theme() + [callback() for callback in main.hooks["config_save"]] + config_dialog.apply_theme() conf.save(nice=1) self.hide() @@ -950,7 +948,7 @@ categories = ["favourite", ] # timer, links, search, and links show up as needed current = "favourite" default = "favourite" - streams = {"favourite":[], "search":[], "scripts":[], "timer":[], } + streams = {"favourite":[], "search":[], "scripts":[], "timer":[], "history":[], } # cache list, to determine if a PLS url is bookmarked @@ -964,10 +962,9 @@ return self.streams.get(cat, []) - # initial display + # streams are already loaded at instantiation def first_show(self): - if not self.streams["favourite"]: - self.cache() + pass # all entries just come from "bookmarks.json" @@ -975,7 +972,9 @@ # stream list cache = conf.load(self.module) if (cache): + __print__(dbg.PROC, "load bookmarks.json") self.streams = cache + # save to cache file @@ -1034,6 +1033,7 @@ def heuristic_update(self, updated_channel, updated_category): if not conf.heuristic_bookmark_update: return + __print__(dbg.ERR, "heuristic bookmark update") save = 0 fav = self.streams["favourite"] @@ -1172,6 +1172,7 @@ # run gui_startup(100/100.0) gtk.main() + __print__(dbg.PROC, r" gtk_main_quit ") # invoke command-line interface diff -Nru streamtuner2-2.1.1/t.py streamtuner2-2.1.3/t.py --- streamtuner2-2.1.1/t.py 1970-01-01 00:00:00.000000000 +0000 +++ streamtuner2-2.1.3/t.py 2014-06-22 01:45:54.000000000 +0000 @@ -0,0 +1,93 @@ +from Tkinter import * +from ttk import * + + +# Customized Toplevel window +class Window(Tk,Toplevel): + window_title = "Example PyTk window" + + # Named widgets + w = {} + + # Nested list of menu entries + menu_struct = [ + # (widget_id, Tk.Widget, submenu_struct[]) + ("file", Menu, dict(label="File", background="#654"), [ + ("save", Menu, dict(label="Save", command=None, background="#543"), []), + ("quit", Menu, dict(label="Quit", command=lambda:main.destroy()), []), + ]), + ("edit", Menu, dict(label="Edit"), []), + ("options", Menu, dict(label="Options"), []) + ] + + # Structuring default widgets + layout = [ + # (widget_id, Tk.Widget, Tk.**args, coord_props, prop_calls(), subwidgets[]) + ("frame", Frame, dict(padding=(5,5,1,1), borderwidth=1, relief="sunken", width=200, height=100), + (0,0,"NESW",1,1), {"columnconfigure":{"index":0,"weight":1}, "rowconfigure":{"index":1,"weight":1}}, + [ + ("label_1", Label, dict(text="Label 1"), (1,0,"NW"), {}, []), + ("label_2", Label, dict(text="Label 2"), (1,1,"ES"), {}, []), + ("label_3", Label, dict(text="Label 3"), (2,0), {}, []), + ("sizegrip", Sizegrip, dict(), (3,5,"SE"), {}, []), + ]), + ] + coord_props = ("row", "column", "sticky", "rowspan", "columnspan") + + # Create window and widgets + def __init__(self, super=Tk): + super.__init__(self) + self.title(string=self.window_title) + self.option_add("*tearOff", FALSE) + self.geometry("300x200") + + #help(Style) + #help(Menu) + s=Style() + s.configure(".", background="#ccddff", font=("Ubuntu", 10)) + + # add menu + self.menu = Menu(self, tearoff=FALSE, relief=FLAT, background="#665544", foreground="#ffffff") + self["menu"] = self.menu + self.menu_construct(self.menu, self.menu_struct) + + # pack widgets + self.widget_construct(self, self.layout) + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + # Construct submenus + def menu_construct(self, parent, menu_struct): + for (id, tkwidget, args, sub_struct) in menu_struct: + self.w[id] = tkwidget(parent) + if "command" in args: + parent.add_command(**args) + else: + parent.add_cascade(menu=self.w[id], **args) + self.menu_construct(self.w[id], sub_struct) + + # Instantiate widgets, add options + def widget_construct(self, parent, layout_struct): + for (id, tkwidget, args, coord, calls, sub_struct) in layout_struct: + self.w[id] = tkwidget(parent, **args) + self.widget_pack(self.w[id], coord) + self.widget_calls(self.w[id], calls) + self.widget_construct(self.w[id], sub_struct) + + # Grid packing + def widget_pack(self, widget, coord): + if type(coord) is not dict: + coord = dict(zip(self.coord_props[0:len(coord)], coord)) + if not "sticky" in coord: + coord["sticky"] = (N,S,E,W) + widget.grid(**coord) + + # Call property methods + def widget_calls(self, widget, calls): + for func,args in calls.items(): + getattr(widget, func)(**args) + + +main = Window() +main.mainloop() +