Jazz"""
+re_category = re.compile('()([^:]+?)()', re.I|re.M)
+
+#re_stream = re.compile('^(.*)')
+#re_description = re.compile('^ (.*?)')
+"""Atlanta Blue Sky - Rock and alternative streaming audio. Live real-time requests."""
+re_stream_desc = re.compile('^([^<>]+)( - )?([^<>\n\r]+)', re.M|re.I)
+
+
+######
+
+
+# Google Stations is actually now DMOZ Stations
+class google(ChannelPlugin):
+
+ # description
+ title = "Google"
+ module = "google"
+ homepage = GOOGLE_STATIONS_HOME
+ version = 0.2
+
+ # config data
+ config = [
+# {"name": "theme", "type": "text", "value":"Tactile", "description":"Streamtuner2 theme; no this isn't a google-specific option. But you know, the plugin options are a new toy."},
+# {"name": "flag2", "type": "boolean", "value":1, "description":"oh see, an unused checkbox"}
+ ]
+
+
+ # category map
+ categories = ['Google/DMOZ Stations', 'Alternative', 'Ambient', 'Classical', 'College', 'Country', 'Dance', 'Experimental', 'Gothic', 'Industrial', 'Jazz', 'Local', 'Lounge', 'Metal', 'New Age', 'Oldies', 'Old-Time Radio', 'Pop', 'Punk', 'Rock', '80s', 'Soundtracks', 'Talk', 'Techno', 'Urban', 'Variety', 'World']
+ catmap = [('Google/DMOZ Stations', '__main', '/Arts/Radio/Internet/'), ['Alternative', 'Alternative', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Alternative/'], ['Ambient', 'Ambient', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Ambient/'], ['Classical', 'Classical', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Classical/'], ['College', 'College', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/College/'], ['Country', 'Country', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Country/'], ['Dance', 'Dance', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Dance/'], ['Experimental', 'Experimental', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Experimental/'], ['Gothic', 'Gothic', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Gothic/'], ['Industrial', 'Industrial', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Industrial/'], ['Jazz', 'Jazz', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Jazz/'], ['Local', 'Local', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Local/'], ['Lounge', 'Lounge', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Lounge/'], ['Metal', 'Metal', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Metal/'], ['New Age', 'New Age', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/New_Age/'], ['Oldies', 'Oldies', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Oldies/'], ['Old-Time Radio', 'Old-Time Radio', '/Arts/Radio/Formats/Old-Time_Radio/Streaming_MP3_Stations/'], ['Pop', 'Pop', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Pop/'], ['Punk', 'Punk', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Punk/'], ['Rock', 'Rock', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/'], ['80s', '80s', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/80s/'], ['Soundtracks', 'Soundtracks', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Soundtracks/'], ['Talk', 'Talk', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Talk/'], ['Techno', 'Techno', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Dance/Techno/'], ['Urban', 'Urban', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Urban/'], ['Variety', 'Variety', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Variety/'], ['World', 'World', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/World/']]
+
+
+ #def __init__(self, parent):
+ # #self.update_categories()
+ # ChannelPlugin.__init__(self, parent)
+
+
+ # refresh category list
+ def update_categories(self):
+
+ # interim data structure for categories (label, google-id/name, url)
+ categories = [
+ ("Google/DMOZ Stations", "__main", "/Arts/Radio/Internet/"),
+ ]
+
+ # fetch and extract list
+ html = http.get(GOOGLE_DIRECTORY_ROOT + CATEGORIES_URL_POSTFIX)
+
+ for row in re_category.findall(html):
+ if row:
+ name = entity_decode(row[2])
+ label = name
+
+ href = entity_decode(row[0])
+ if href[0] != "/":
+ href = CATEGORIES_URL_POSTFIX + href
+
+ categories.append([label, name, href])
+
+ # return
+ self.catmap = categories
+ self.categories = [x[0] for x in categories]
+ pass
+ # actually saving this into _categories and _catmap.json would be nice
+ # ...
+
+
+
+ # download links from dmoz listing
+ def update_streams(self, cat, force=0):
+
+ # result list
+ ls = []
+
+ # get //dmoz.org/HREF for category name
+ try:
+ (label, name, href) = [x for x in self.catmap if x[0]==cat][0]
+ except:
+ return ls # wrong category
+
+ # download
+ html = http.get(GOOGLE_DIRECTORY_ROOT + href)
+
+ # filter
+ for row in re_stream_desc.findall(html):
+
+ if row:
+ row = {
+ "homepage": entity_decode(row[0]),
+ "title": entity_decode(row[1]),
+ "playing": entity_decode(row[3]),
+ }
+ ls.append(row)
+
+
+ # final list for current category
+ return ls
+
+
+
ADDED channels/internet_radio_org_uk.py
Index: channels/internet_radio_org_uk.py
==================================================================
--- channels/internet_radio_org_uk.py
+++ channels/internet_radio_org_uk.py
@@ -0,0 +1,151 @@
+#
+# api: streamtuner2
+# title: internet-radio.org.uk
+# description: io channel
+# version: 0.1
+#
+#
+# Might become new main plugin
+#
+#
+#
+
+
+
+from channels import *
+import re
+from config import conf
+import http
+from pq import pq
+
+
+
+
+# streams and gui
+class internet_radio_org_uk (ChannelPlugin):
+
+
+ # description
+ title = "InternetRadio"
+ module = "internet_radio_org_uk"
+ homepage = "http://www.internet-radio.org.uk/"
+ version = 0.1
+ listformat = "audio/x-scpls"
+
+ # settings
+ config = [
+ {"name":"internetradio_max_pages", "type":"int", "value":5, "description":"How many pages to fetch and read."},
+ ]
+
+
+ # category map
+ categories = []
+ current = ""
+ default = ""
+
+
+ # load genres
+ def update_categories(self):
+
+ html = http.get(self.homepage)
+ rx = re.compile("""""")
+
+ self.categories = rx.findall(html)
+
+
+
+
+
+ # fetch station lists
+ def update_streams(self, cat, force=0):
+
+ entries = []
+ if cat not in self.categories:
+ return []
+
+ # regex
+ #rx_div = re.compile('(.+?) ', re.S)
+ rx_data = re.compile("""
+ (?:M3U|PLS)',\s*'(http://[^']+)'
+ .*?
+
([^\n]*?) |
+ .*?
+ (?:href="(http://[^"]+)"[^>]+target="_blank"[^>]*)?
+ >\s*
+ \s*(\w[^<]+)[<\n]
+ .*?
+ playing\s*:\s*([^<\n]+)
+ .*?
+ (\d+)\s*Kbps
+ (?:
(\d+)\s*Listeners)?
+ """, re.S|re.X)
+ #rx_homepage = re.compile('href="(http://[^"]+)"[^>]+target="_blank"')
+ rx_pages = re.compile('href="/stations/[-+\w%\d\s]+/page(\d+)">\d+')
+ rx_numbers = re.compile("(\d+)")
+ self.parent.status("downloading category pages...")
+
+
+ # multiple pages
+ page = 1
+ max = int(conf.internetradio_max_pages)
+ max = (max if max > 1 else 1)
+ while page <= max:
+
+ # fetch
+ html = http.get(self.homepage + "stations/" + cat.lower().replace(" ", "%20") + "/" + ("page"+str(page) if page>1 else ""))
+
+
+ # regex parsing?
+ if not conf.pyquery:
+ # step through
+ for uu in rx_data.findall(html):
+ (url, genre, homepage, title, playing, bitrate, listeners) = uu
+
+ # transform data
+ entries.append({
+ "url": url,
+ "genre": self.strip_tags(genre),
+ "homepage": http.fix_url(homepage),
+ "title": title,
+ "playing": playing,
+ "bitrate": int(bitrate),
+ "listeners": int(listeners if listeners else 0),
+ "format": "audio/mp3", # there is no stream info on that, but internet-radio.org.uk doesn't seem very ogg-friendly anyway, so we assume the default here
+ })
+
+ # DOM parsing
+ else:
+ # the streams are arranged in table rows
+ doc = pq(html)
+ for dir in (pq(e) for e in doc("tr.stream")):
+
+ bl = dir.find("td[align=right]").text()
+ bl = rx_numbers.findall(str(bl) + " 0 0")
+
+ entries.append({
+ "title": dir.find("b").text(),
+ "homepage": http.fix_url(dir.find("a.url").attr("href")),
+ "url": dir.find("a").eq(2).attr("href"),
+ "genre": dir.find("td").eq(0).text(),
+ "bitrate": int(bl[0]),
+ "listeners": int(bl[1]),
+ "format": "audio/mp3",
+ "playing": dir.find("td").eq(1).children().remove().end().text()[13:].strip(),
+ })
+
+ # next page?
+ if str(page+1) not in rx_pages.findall(html):
+ max = 0
+ else:
+ page = page + 1
+
+ # keep listview updated while searching
+ self.update_streams_partially_done(entries)
+ try: self.parent.status(float(page)/float(max))
+ except: """there was a div by zero bug report despite max=1 precautions"""
+
+ # fin
+ self.parent.status()
+ return entries
+
+
ADDED channels/jamendo.py
Index: channels/jamendo.py
==================================================================
--- channels/jamendo.py
+++ channels/jamendo.py
@@ -0,0 +1,160 @@
+
+# api: streamtuner2
+# title: jamendo browser
+#
+# For now this is really just a browser, doesn't utilizt the jamendo API yet.
+# Requires more rework of streamtuner2 list display to show album covers.
+#
+
+
+import re
+import http
+from config import conf
+from channels import *
+from xml.sax.saxutils import unescape
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# jamendo CC music sharing site
+class jamendo (ChannelPlugin):
+
+ # description
+ title = "Jamendo"
+ module = "jamendo"
+ homepage = "http://www.jamendo.com/"
+ version = 0.2
+
+ base = "http://www.jamendo.com/en/"
+ listformat = "url/http"
+
+ categories = [] #"top 100", "reload category tree!", ["menu > channel > reload.."]]
+ titles = dict( title="Artist", playing="Album/Song", bitrate=False, listeners=False )
+
+ config = [
+ {"name":"jamendo_stream_format", "value":"ogg2", "type":"text", "description":"streaming format, 'ogg2' or 'mp31'"}
+ ]
+
+
+
+
+ # refresh category list
+ def update_categories(self):
+
+ html = http.get(self.base + "tags")
+
+ rx_current = re.compile(r"""
+ ]+rel="tag"[^>]+href="(http://www.jamendo.com/\w\w/tag/[\w\d]+)"[^>]*>([\w\d]+)
+ """, re.S|re.X)
+
+
+ #-- categories
+ tags = []
+ for uu in rx_current.findall(html):
+ (href, title) = uu
+ tags.append(title)
+
+ self.categories = [
+ "top 100",
+ "radios",
+ "tags", tags
+ ]
+
+
+
+ # download links from dmoz listing
+ def update_streams(self, cat, force=0):
+
+ entries = []
+
+ # top list
+ if cat == "top" or cat == "top 100":
+ html = http.get(self.base + "top")
+
+ rx_top = re.compile("""
+ ]+src="(http://imgjam.com/albums/[\w\d]+/\d+/covers/1.\d+.jpg)"
+ .*?
+
+ \s*]+src="(http://imgjam.com/albums/[\w\d]+/\d+/covers/1.\d+.jpg)"
+ .*? /tag/([\w\d]+)"
+ """, re.X|re.S)
+
+ for uu in rx_tag.findall(html):
+ (artist, title, album, album_id, cover, tag) = uu
+ entries.append({
+ "title": artist,
+ "playing": title,
+ "homepage": album,
+ "url": self.track_url(album_id, conf.jamendo_stream_format, "album"),
+ "favicon": self.cover(cover),
+ "genre": tag,
+ "format": self.stream_mime(),
+ })
+
+ # done
+ return entries
+
+
+ # smaller album link
+ def cover(self, url):
+ return url.replace(".100",".50").replace(".130",".50")
+
+ # track id to download url
+ def track_url(self, track_id, fmt="ogg2", track="track", urltype="redirect"):
+ # track = "album"
+ # fmt = "mp31"
+ # urltype = "m3u"
+ return "http://api.jamendo.com/get2/stream/"+track+"/"+urltype+"/?id="+track_id+"&streamencoding="+fmt
+
+ # audio/*
+ def stream_mime(self):
+ if conf.jamendo_stream_format.find("og") >= 0:
+ return "audio/ogg"
+ else:
+ return "audio/mp3"
+
+
ADDED channels/links.py
Index: channels/links.py
==================================================================
--- channels/links.py
+++ channels/links.py
@@ -0,0 +1,78 @@
+#
+# api: streamtuner2
+# title: links to directory services
+# description: provides a simple list of homepages for directory services
+# version: 0.1
+# priority: rare
+#
+#
+# Simply adds a "links" entry in bookmarks tab, where known channels
+# and some others are listed with homepage links.
+#
+#
+
+
+from channels import *
+import copy
+
+
+
+# hooks into main.bookmarks
+class links (object):
+
+ # plugin info
+ module = "links"
+ title = "Links"
+ version = 0.1
+
+
+ # configuration settings
+ config = [ ]
+
+ # list
+ streams = [ ]
+ default = {
+ "radio.de": "http://www.radio.de/",
+ "musicgoal": "http://www.musicgoal.com/",
+ "streamfinder": "http://www.streamfinder.com/",
+ "last.fm": "http://www.last.fm/",
+ "rhapsody (US-only)": "http://www.rhapsody.com/",
+ "pandora (US-only)": "http://www.pandora.com/",
+ "radiotower": "http://www.radiotower.com/",
+ "pirateradio": "http://www.pirateradionetwork.com/",
+ "R-L": "http://www.radio-locator.com/",
+ "radio station world": "http://radiostationworld.com/",
+ "surfmusik.de": "http://www.surfmusic.de/",
+ }
+
+
+ # prepare gui
+ def __init__(self, parent):
+ if parent:
+
+ # target channel
+ bookmarks = parent.bookmarks
+ if not bookmarks.streams.get(self.module):
+ bookmarks.streams[self.module] = []
+ bookmarks.add_category(self.module)
+
+
+ # collect links from channel plugins
+ for name,channel in parent.channels.iteritems():
+ try:
+ self.streams.append({
+ "favourite": 1,
+ "title": channel.title,
+ "homepage": channel.homepage,
+ })
+ except: pass
+ for title,homepage in self.default.iteritems():
+ self.streams.append({
+ "title": title,
+ "homepage": homepage,
+ })
+
+ # add to bookmarks
+ bookmarks.streams[self.module] = self.streams
+
+
ADDED channels/live365.py
Index: channels/live365.py
==================================================================
--- channels/live365.py
+++ channels/live365.py
@@ -0,0 +1,161 @@
+
+
+
+
+
+# streamtuner2 modules
+from config import conf
+from mygtk import mygtk
+import http
+from channels import *
+from channels import __print__
+
+# python modules
+import re
+import xml.dom.minidom
+from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities
+import gtk
+import copy
+import urllib
+
+
+# channel live365
+class live365(ChannelPlugin):
+
+
+ # desc
+ api = "streamtuner2"
+ module = "live365"
+ title = "Live365"
+ version = 0.1
+ homepage = "http://www.live365.com/"
+ base_url = "http://www.live365.com/"
+ listformat = "url/http"
+ mediatype = "audio/mpeg"
+
+ # content
+ categories = ['Alternative', ['Britpop', 'Classic Alternative', 'College', 'Dancepunk', 'Dream Pop', 'Emo', 'Goth', 'Grunge', 'Indie Pop', 'Indie Rock', 'Industrial', 'Lo-Fi', 'Modern Rock', 'New Wave', 'Noise Pop', 'Post-Punk', 'Power Pop', 'Punk'], 'Blues', ['Acoustic Blues', 'Chicago Blues', 'Contemporary Blues', 'Country Blues', 'Delta Blues', 'Electric Blues', 'Cajun/Zydeco'], 'Classical', ['Baroque', 'Chamber', 'Choral', 'Classical Period', 'Early Classical', 'Impressionist', 'Modern', 'Opera', 'Piano', 'Romantic', 'Symphony'], 'Country', ['Alt-Country', 'Americana', 'Bluegrass', 'Classic Country', 'Contemporary Bluegrass', 'Contemporary Country', 'Honky Tonk', 'Hot Country Hits', 'Western'], 'Easy Listening', ['Exotica', 'Lounge', 'Orchestral Pop', 'Polka', 'Space Age Pop'], 'Electronic/Dance', ['Acid House', 'Ambient', 'Big Beat', 'Breakbeat', 'Disco', 'Downtempo', "Drum 'n' Bass", 'Electro', 'Garage', 'Hard House', 'House', 'IDM', 'Jungle', 'Progressive', 'Techno', 'Trance', 'Tribal', 'Trip Hop'], 'Folk', ['Alternative Folk', 'Contemporary Folk', 'Folk Rock', 'New Acoustic', 'Traditional Folk', 'World Folk'], 'Freeform', ['Chill', 'Experimental', 'Heartache', 'Love/Romance', 'Music To ... To', 'Party Mix', 'Patriotic', 'Rainy Day Mix', 'Reality', 'Shuffle/Random', 'Travel Mix', 'Trippy', 'Various', 'Women', 'Work Mix'], 'Hip-Hop/Rap', ['Alternative Rap', 'Dirty South', 'East Coast Rap', 'Freestyle', 'Gangsta Rap', 'Old School', 'Turntablism', 'Underground Hip-Hop', 'West Coast Rap'], 'Inspirational', ['Christian', 'Christian Metal', 'Christian Rap', 'Christian Rock', 'Classic Christian', 'Contemporary Gospel', 'Gospel', 'Praise/Worship', 'Sermons/Services', 'Southern Gospel', 'Traditional Gospel'], 'International', ['African', 'Arabic', 'Asian', 'Brazilian', 'Caribbean', 'Celtic', 'European', 'Filipino', 'Greek', 'Hawaiian/Pacific', 'Hindi', 'Indian', 'Japanese', 'Jewish', 'Mediterranean', 'Middle Eastern', 'North American', 'Soca', 'South American', 'Tamil', 'Worldbeat', 'Zouk'], 'Jazz', ['Acid Jazz', 'Avant Garde', 'Big Band', 'Bop', 'Classic Jazz', 'Cool Jazz', 'Fusion', 'Hard Bop', 'Latin Jazz', 'Smooth Jazz', 'Swing', 'Vocal Jazz', 'World Fusion'], 'Latin', ['Bachata', 'Banda', 'Bossa Nova', 'Cumbia', 'Latin Dance', 'Latin Pop', 'Latin Rap/Hip-Hop', 'Latin Rock', 'Mariachi', 'Merengue', 'Ranchera', 'Salsa', 'Tango', 'Tejano', 'Tropicalia'], 'Metal', ['Extreme Metal', 'Heavy Metal', 'Industrial Metal', 'Pop Metal/Hair', 'Rap Metal'], 'New Age', ['Environmental', 'Ethnic Fusion', 'Healing', 'Meditation', 'Spiritual'], 'Oldies', ['30s', '40s', '50s', '60s', '70s', '80s', '90s'], 'Pop', ['Adult Contemporary', 'Barbershop', 'Bubblegum Pop', 'Dance Pop', 'JPOP', 'Soft Rock', 'Teen Pop', 'Top 40', 'World Pop'], 'R&B/Urban', ['Classic R&B', 'Contemporary R&B', 'Doo Wop', 'Funk', 'Motown', 'Neo-Soul', 'Quiet Storm', 'Soul', 'Urban Contemporary'], 'Reggae', ['Contemporary Reggae', 'Dancehall', 'Dub', 'Pop-Reggae', 'Ragga', 'Reggaeton', 'Rock Steady', 'Roots Reggae', 'Ska'], 'Rock', ['Adult Album Alternative', 'British Invasion', 'Classic Rock', 'Garage Rock', 'Glam', 'Hard Rock', 'Jam Bands', 'Prog/Art Rock', 'Psychedelic', 'Rock & Roll', 'Rockabilly', 'Singer/Songwriter', 'Surf'], 'Seasonal/Holiday', ['Anniversary', 'Birthday', 'Christmas', 'Halloween', 'Hanukkah', 'Honeymoon', 'Valentine', 'Wedding'], 'Soundtracks', ['Anime', "Children's/Family", 'Original Score', 'Showtunes'], 'Talk', ['Comedy', 'Community', 'Educational', 'Government', 'News', 'Old Time Radio', 'Other Talk', 'Political', 'Scanner', 'Spoken Word', 'Sports']]
+ current = ""
+ default = "Pop"
+ empty = None
+
+ # redefine
+ streams = {}
+
+
+ def __init__(self, parent=None):
+
+ # override datamap fields //@todo: might need a cleaner method, also: do we really want the stream data in channels to be different/incompatible?
+ self.datamap = copy.deepcopy(self.datamap)
+ self.datamap[5][0] = "Rating"
+ self.datamap[5][2][0] = "rating"
+ self.datamap[3][0] = "Description"
+ self.datamap[3][2][0] = "description"
+
+ # superclass
+ ChannelPlugin.__init__(self, parent)
+
+
+ # read category thread from /listen/browse.live
+ def update_categories(self):
+ self.categories = []
+
+ # fetch page
+ html = http.get("http://www.live365.com/index.live", feedback=self.parent.status);
+ rx_genre = re.compile("""
+ href='/genres/([\w\d%+]+)'[^>]*>
+ ( (?:)? )
+ ( \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)
+
+
+
+ # extract stream infos
+ def update_streams(self, cat, search=""):
+
+ # search / url
+ if (not search):
+ url = "http://www.live365.com/cgi-bin/directory.cgi?genre=" + self.cat2tag(cat) + "&rows=200" #+"&first=1"
+ else:
+ url = "http://www.live365.com/cgi-bin/directory.cgi?site=..&searchdesc=" + urllib.quote(search) + "&searchgenre=" + self.cat2tag(cat) + "&x=0&y=0"
+ html = http.get(url, feedback=self.parent.status)
+ # we only need to download one page, because live365 always only gives 200 results
+
+ # terse format
+ rx = re.compile(r"""
+ ['"]Launch\((\d+).*?
+ ['"](OK|PM_ONLY|SUBSCRIPTION).*?
+ href=['"](http://www.live365.com/stations/\w+)['"].*?
+ page['"]>([^<>]*).*?
+ CLASS="genre"[^>]*>(.+?).+?
+ =["']audioQuality.+?>\w+\s+(\d+)\w<.+?
+ >DrawListenerStars\((\d+),.+?
+ >DrawRatingStars\((\d+),\s+(\d+),.*?
+ ["']station_id=(\d+).+?
+ class=["']?desc-link[^>]+>([^<>]*)<
+ """, re.X|re.I|re.S|re.M)
+# src="(http://www.live365.com/.+?/stationlogo\w+.jpg)".+?
+
+ # append entries to result list
+ __print__( html )
+ ls = []
+ for row in rx.findall(html):
+ __print__( row )
+ points = int(row[7])
+ count = int(row[8])
+ ls.append({
+ "launch_id": row[0],
+ "sofo": row[1], # subscribe-or-fuck-off status flags
+ "state": ("" if row[1]=="OK" else gtk.STOCK_STOP),
+ "homepage": entity_decode(row[2]),
+ "title": entity_decode(row[3]),
+ "genre": self.strip_tags(row[4]),
+ "bitrate": int(row[5]),
+ "listeners": int(row[6]),
+ "max": 0,
+ "rating": (points + count**0.4) / (count - 0.001*(count-0.1)), # prevents division by null, and slightly weights (more votes are higher scored than single votes)
+ "rating_points": points,
+ "rating_count": count,
+ # id for URL:
+ "station_id": row[9],
+ "url": self.base_url + "play/" + row[9],
+ "description": entity_decode(row[10]),
+ #"playing": row[10],
+ # "deleted": row[0] != "OK",
+ })
+ return ls
+
+ # faster if we do it in _update() prematurely
+ #def prepare(self, ls):
+ # GenericChannel.prepare(ls)
+ # for row in ls:
+ # if (not row["state"]):
+ # row["state"] = (gtk.STOCK_STOP, "") [row["sofo"]=="OK"]
+ # return ls
+
+
+ # html helpers
+ def cat2tag(self, cat):
+ return urllib.quote(cat.lower()) #re.sub("[^a-z]", "",
+ def strip_tags(self, s):
+ return re.sub("<.+?>", "", s)
+
+
ADDED channels/modarchive.py
Index: channels/modarchive.py
==================================================================
--- channels/modarchive.py
+++ channels/modarchive.py
@@ -0,0 +1,129 @@
+
+# api: streamtuner2
+# title: modarchive browser
+#
+#
+# Just a genre browser.
+#
+# MOD files dodn't work with all audio players. And with the default
+# download method, it'll receive a .zip archive with embeded .mod file.
+# VLC in */* seems to work fine however.
+#
+
+
+import re
+import http
+from config import conf
+from channels import *
+from channels import __print__
+from xml.sax.saxutils import unescape
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# MODs
+class modarchive (ChannelPlugin):
+
+ # description
+ title = "modarchive"
+ module = "modarchive"
+ homepage = "http://www.modarchive.org/"
+ version = 0.1
+ base = "http://modarchive.org/"
+
+ # keeps category titles->urls
+ catmap = {}
+ categories = []
+
+
+
+
+ # refresh category list
+ def update_categories(self):
+
+ html = http.get("http://modarchive.org/index.php?request=view_genres")
+
+ rx_current = re.compile(r"""
+ >\s+(\w[^<>]+)\s+ |
+ ]+query=(\d+)&[^>]+>(\w[^<]+)
+ """, re.S|re.X)
+
+
+ #-- archived shows
+ sub = []
+ self.categories = []
+ for uu in rx_current.findall(html):
+ (main, id, subname) = uu
+ if main:
+ if sub:
+ self.categories.append(sub)
+ sub = []
+ self.categories.append(main)
+ else:
+ sub.append(subname)
+ self.catmap[subname] = id
+ #
+
+ #-- keep catmap as cache-file, it's essential for redisplaying
+ self.save()
+ return
+
+
+ # saves .streams and .catmap
+ def save(self):
+ ChannelPlugin.save(self)
+ conf.save("cache/catmap_" + self.module, self.catmap)
+
+
+ # read previous channel/stream data, if there is any
+ def cache(self):
+ ChannelPlugin.cache(self)
+ # catmap
+ cache = conf.load("cache/catmap_" + self.module)
+ if (cache):
+ self.catmap = cache
+ pass
+
+
+ # download links from dmoz listing
+ def update_streams(self, cat, force=0):
+
+ url = "http://modarchive.org/index.php?query="+self.catmap[cat]+"&request=search&search_type=genre"
+ html = http.get(url)
+ entries = []
+
+ rx_mod = re.compile("""
+ href="(http://modarchive.org/data/downloads.php[?]moduleid=(\d+)[#][^"]+)"
+ .*? /formats/(\w+).png"
+ .*? title="([^">]+)">([^<>]+)
+ .*? >Rated\s*(\d+)
+ """, re.X|re.S)
+
+ for uu in rx_mod.findall(html):
+ (url, id, fmt, title, file, rating) = uu
+ __print__( uu )
+ entries.append({
+ "genre": cat,
+ "url": url,
+ "id": id,
+ "format": self.mime_fmt(fmt) + "+zip",
+ "title": title,
+ "playing": file,
+ "listeners": int(rating),
+ "homepage": "http://modarchive.org/index.php?request=view_by_moduleid&query="+id,
+ })
+
+ # done
+ return entries
+
+
ADDED channels/musicgoal.py
Index: channels/musicgoal.py
==================================================================
--- channels/musicgoal.py
+++ channels/musicgoal.py
@@ -0,0 +1,102 @@
+#
+# api: streamtuner2
+# title: MUSICGOAL channel
+# description: musicgoal.com/.de combines radio and podcast listings
+# version: 0.1
+# status: experimental
+# pre-config:
+#
+# Musicgoal.com is a radio and podcast directory. This plugin tries to use
+# the new API for accessing listing data.
+#
+#
+
+
+
+# st2 modules
+from config import conf
+from mygtk import mygtk
+import http
+from channels import *
+
+# python modules
+import re
+import json
+
+
+
+
+# I wonder what that is for ---------------------------------------
+class musicgoal (ChannelPlugin):
+
+ # desc
+ module = "musicgoal"
+ title = "MUSICGOAL"
+ version = 0.1
+ homepage = "http://www.musicgoal.com/"
+ base_url = homepage
+ listformat = "url/direct"
+
+ # settings
+ config = [
+ ]
+ api_podcast = "http://www.musicgoal.com/api/?todo=export&todo2=%s&cat=%s&format=json&id=1000259223&user=streamtuner&pass=tralilala"
+ api_radio = "http://www.musicgoal.com/api/?todo=playlist&format=json&genre=%s&id=1000259223&user=streamtuner&pass=tralilala"
+
+ # categories are hardcoded
+ podcast = ["Arts", "Management", "Recreation", "Knowledge", "Nutrition", "Books", "Movies & TV", "Music", "News", "Business", "Poetry", "Politic", "Radio", "Science", "Science Fiction", "Religion", "Sport", "Technic", "Travel", "Health", "New"]
+ radio = ["Top radios", "Newcomer", "Alternative", "House", "Jazz", "Classic", "Metal", "Oldies", "Pop", "Rock", "Techno", "Country", "Funk", "Hip hop", "R&B", "Reggae", "Soul", "Indian", "Top40", "60s", "70s", "80s", "90s", "Sport", "Various", "Radio", "Party", "Christmas", "Firewall", "Auto DJ", "Audio-aacp", "Audio-ogg", "Video", "MyTop", "New", "World", "Full"]
+ categories = ["podcasts/", podcast, "radios/", radio]
+ #catmap = {"podcast": dict((i+1,v) for enumerate(self.podcast)), "radio": dict((i+1,v) for enumerate(self.radio))}
+
+
+
+ # nop
+ def update_categories(self):
+ pass
+
+
+ # request json API
+ def update_streams(self, cat, search=""):
+
+ # category type: podcast or radio
+ if cat in self.podcast:
+ grp = "podcast"
+ url = self.api_podcast % (grp, self.podcast.index(cat)+1)
+ elif cat in self.radio:
+ grp = "radio"
+ url = self.api_radio % cat.lower().replace(" ","").replace("&","")
+ else:
+ return []
+
+ # retrieve API data
+ data = http.ajax(url, None)
+ data = json.loads(data)
+
+ # tranform datasets
+ if grp == "podcast":
+ return [{
+ "genre": cat,
+ "title": row["titel"],
+ "homepage": row["url"],
+ "playing": str(row["typ"]),
+ #"id": row["id"],
+ #"listeners": int(row["2"]),
+ #"listformat": "text/html",
+ "url": "",
+ } for row in data]
+ else:
+ return [{
+ "format": self.mime_fmt(row["ctype"]),
+ "genre": row["genre"] or cat,
+ "url": "http://%s:%s/%s" % (row["host"], row["port"], row["pfad"]),
+ "listformat": "url/direct",
+ "id": row["id"],
+ "title": row["name"],
+ "playing": row["song"],
+ "homepage": row.get("homepage") or row.get("url"),
+ } for row in data]
+
+
+
+
ADDED channels/myoggradio.py
Index: channels/myoggradio.py
==================================================================
--- channels/myoggradio.py
+++ channels/myoggradio.py
@@ -0,0 +1,185 @@
+#
+# api: streamtuner2
+# title: MyOggRadio channel plugin
+# description: open source internet radio directory MyOggRadio
+# version: 0.5
+# config:
+#
+# priority: standard
+# category: channel
+# depends: json, StringIO
+#
+# MyOggRadio is an open source radio station directory. Because this matches
+# well with streamtuner2, there's now a project partnership. Shared streams can easily
+# be downloaded in this channel plugin. And streamtuner2 users can easily share their
+# favourite stations into the MyOggRadio directory.
+#
+# Beforehand an account needs to be configured in the settings. (Registration
+# on myoggradio doesn't require an email address or personal information.)
+#
+
+
+
+from channels import *
+from config import conf
+from action import action
+
+import re
+import json
+from StringIO import StringIO
+import copy
+
+
+
+# open source radio sharing stie
+class myoggradio(ChannelPlugin):
+
+ # description
+ title = "MyOggRadio"
+ module = "myoggradio"
+ homepage = "http://www.myoggradio.org/"
+ api = "http://ehm.homelinux.org/MyOggRadio/"
+ version = 0.5
+ listformat = "url/direct"
+
+ # config data
+ config = [
+ {"name":"myoggradio_login", "type":"text", "value":"user:password", "description":"Account for storing personal favourites."},
+ {"name":"myoggradio_morph", "type":"boolean", "value":0, "description":"Convert pls/m3u into direct shoutcast url."},
+ ]
+
+ # hide unused columns
+ titles = dict(playing=False, listeners=False, bitrate=False)
+
+
+ # category map
+ categories = ['common', 'personal']
+ default = 'common'
+ current = 'common'
+
+
+
+ # prepare GUI
+ def __init__(self, parent):
+ ChannelPlugin.__init__(self, parent)
+ if parent:
+ mygtk.add_menu(parent.extensions, "Share in MyOggRadio", self.share)
+
+
+
+ # this is simple, there are no categories
+ def update_categories(self):
+ pass
+
+
+
+ # download links from dmoz listing
+ def update_streams(self, cat, force=0):
+
+ # result list
+ entries = []
+
+ # common
+ if (cat == "common"):
+ # fetch
+ data = http.get(self.api + "common.json")
+ entries = json.load(StringIO(data))
+
+ # bookmarks
+ elif (cat == "personal") and self.user_pw():
+ data = http.get(self.api + "favoriten.json?user=" + self.user_pw()[0])
+ entries = json.load(StringIO(data))
+
+ # unknown
+ else:
+ self.parent.status("Unknown category")
+ pass
+
+ # augment result list
+ for i,e in enumerate(entries):
+ entries[i]["homepage"] = self.api + "c_common_details.jsp?url=" + e["url"]
+ entries[i]["genre"] = cat
+ # send back
+ return entries
+
+
+
+ # upload a single station entry to MyOggRadio
+ def share(self, *w):
+
+ # get data
+ row = self.parent.row()
+ if row:
+ row = copy.copy(row)
+
+ # convert PLS/M3U link to direct ICY stream url
+ if conf.myoggradio_morph and self.parent.channel().listformat != "url/direct":
+ row["url"] = http.fix_url(action.srv(row["url"]))
+
+ # prevent double check-ins
+ if row["title"] in (r.get("title") for r in self.streams["common"]):
+ pass
+ elif row["url"] in (r.get("url") for r in self.streams["common"]):
+ pass
+
+ # send
+ else:
+ self.parent.status("Sharing station URL...")
+ self.upload(row)
+ sleep(0.5) # artificial slowdown, else user will assume it didn't work
+
+ # tell Gtk we've handled the situation
+ self.parent.status("Shared '" + row["title"][:30] + "' on MyOggRadio.org")
+ return True
+
+
+ # upload bookmarks
+ def send_bookmarks(self, entries=[]):
+
+ for e in (entries if entries else parent.bookmarks.streams["favourite"]):
+ self.upload(e)
+
+
+ # send row to MyOggRadio
+ def upload(self, e, form=0):
+ if e:
+ login = self.user_pw()
+ submit = {
+ "user": login[0], # api
+ "passwort": login[1], # api
+ "url": e["url"],
+ "bemerkung": e["title"],
+ "genre": e["genre"],
+ "typ": e["format"][6:],
+ "eintragen": "eintragen", # form
+ }
+
+ # just push data in, like the form does
+ if form:
+ self.login()
+ http.ajax(self.api + "c_neu.jsp", submit)
+
+ # use JSON interface
+ else:
+ http.ajax(self.api + "commonadd.json?" + urllib.urlencode(submit))
+
+
+ # authenticate against MyOggRadio
+ def login(self):
+ login = self.user_pw()
+ if login:
+ data = dict(zip(["benutzer", "passwort"], login))
+ http.ajax(self.api + "c_login.jsp", data)
+ # let's hope the JSESSIONID cookie is kept
+
+
+ # returns login (user,pw)
+ def user_pw(self):
+ if conf.myoggradio_login != "user:password":
+ return conf.myoggradio_login.split(":")
+ else: pass
+
+
+
+
+
ADDED channels/punkcast.py
Index: channels/punkcast.py
==================================================================
--- channels/punkcast.py
+++ channels/punkcast.py
@@ -0,0 +1,102 @@
+
+# api: streamtuner2
+# title: punkcast listing
+#
+#
+# Disables itself per default.
+# ST1 looked prettier with random images within.
+#
+
+
+import re
+import http
+from config import conf
+import action
+from channels import *
+from channels import __print__
+
+
+
+
+
+# disable plugin per default
+if "punkcast" not in vars(conf):
+ conf.plugins["punkcast"] = 0
+
+
+
+
+
+
+
+
+
+# basic.ch broadcast archive
+class punkcast (ChannelPlugin):
+
+ # description
+ title = "punkcast"
+ module = "punkcast"
+ homepage = "http://www.punkcast.com/"
+ version = 0.1
+
+ # keeps category titles->urls
+ catmap = {}
+ categories = ["list"]
+ default = "list"
+ current = "list"
+
+
+
+ # don't do anything
+ def update_categories(self):
+ pass
+
+
+ # get list
+ def update_streams(self, cat, force=0):
+
+ rx_link = re.compile("""
+
+ \s+]+ALT="([^<">]+)"
+ """, re.S|re.X)
+
+ entries = []
+
+ #-- all from frontpage
+ for uu in rx_link.findall(http.get(self.homepage)):
+ (homepage, id, title) = uu
+ entries.append({
+ "genre": "?",
+ "title": title,
+ "playing": "PUNKCAST #"+id,
+ "format": "audio/mp3",
+ "homepage": homepage,
+ })
+
+ # done
+ return entries
+
+
+ # special handler for play
+ def play(self, row):
+
+ rx_sound = re.compile("""(http://[^"<>]+[.](mp3|ogg|m3u|pls|ram))""")
+ html = http.get(row["homepage"])
+
+ # look up ANY audio url
+ for uu in rx_sound.findall(html):
+ __print__( uu )
+ (url, fmt) = uu
+ action.action.play(url, self.mime_fmt(fmt), "url/direct")
+ return
+
+ # or just open webpage
+ action.action.browser(row["homepage"])
+
+
+
+
+
+
+
ADDED channels/shoutcast.py
Index: channels/shoutcast.py
==================================================================
--- channels/shoutcast.py
+++ channels/shoutcast.py
@@ -0,0 +1,202 @@
+#
+# api: streamtuner2
+# title: shoutcast
+# description: Channel/tab for Shoutcast.com directory
+# depends: pq, re, http
+# version: 1.2
+# author: Mario
+# original: Jean-Yves Lefort
+#
+# Shoutcast is a server software for audio streaming. It automatically spools
+# station information on shoutcast.com, which this plugin can read out. But
+# since the website format is often changing, we now use PyQuery HTML parsing
+# in favour of regular expression (which still work, are faster, but not as
+# reliable).
+#
+# This was previously a built-in channel plugin. It just recently was converted
+# from a glade predefined GenericChannel into a ChannelPlugin.
+#
+#
+# NOTES
+#
+# Just found out what Tunapie uses:
+# http://www.shoutcast.com/sbin/newxml.phtml?genre=Top500
+# It's a simpler list format, no need to parse HTML. However, it also lacks
+# homepage links. But maybe useful as alternate fallback...
+# Also:
+# http://www.shoutcast.com/sbin/newtvlister.phtml?alltv=1
+# http://www.shoutcast.com/sbin/newxml.phtml?search=
+#
+#
+#
+
+
+import http
+import urllib
+import re
+from pq import pq
+from config import conf
+#from channels import * # works everywhere but in this plugin(???!)
+import channels
+__print__ = channels.__print__
+
+
+
+# SHOUTcast data module ----------------------------------------
+class shoutcast(channels.ChannelPlugin):
+
+ # desc
+ api = "streamtuner2"
+ module = "shoutcast"
+ title = "SHOUTcast"
+ version = 1.2
+ homepage = "http://www.shoutcast.com/"
+ base_url = "http://shoutcast.com/"
+ listformat = "audio/x-scpls"
+
+ # settings
+ config = [
+ dict(name="pyquery", type="boolean", value=0, description="Use more reliable PyQuery HTML parsing\ninstead of faster regular expressions."),
+ dict(name="debug", type="boolean", value=0, description="enable debug output"),
+ ]
+
+ # categories
+ categories = ['Alternative', ['Adult Alternative', 'Britpop', 'Classic Alternative', 'College', 'Dancepunk', 'Dream Pop', 'Emo', 'Goth', 'Grunge', 'Hardcore', 'Indie Pop', 'Indie Rock', 'Industrial', 'Modern Rock', 'New Wave', 'Noise Pop', 'Power Pop', 'Punk', 'Ska', 'Xtreme'], 'Blues', ['Acoustic Blues', 'Chicago Blues', 'Contemporary Blues', 'Country Blues', 'Delta Blues', 'Electric Blues'], 'Classical', ['Baroque', 'Chamber', 'Choral', 'Classical Period', 'Early Classical', 'Impressionist', 'Modern', 'Opera', 'Piano', 'Romantic', 'Symphony'], 'Country', ['Americana', 'Bluegrass', 'Classic Country', 'Contemporary Bluegrass', 'Contemporary Country', 'Honky Tonk', 'Hot Country Hits', 'Western'], 'Decades', ['30s', '40s', '50s', '60s', '70s', '80s', '90s'], 'Easy Listening', ['Exotica', 'Light Rock', 'Lounge', 'Orchestral Pop', 'Polka', 'Space Age Pop'], 'Electronic', ['Acid House', 'Ambient', 'Big Beat', 'Breakbeat', 'Dance', 'Demo', 'Disco', 'Downtempo', 'Drum and Bass', 'Electro', 'Garage', 'Hard House', 'House', 'IDM', 'Jungle', 'Progressive', 'Techno', 'Trance', 'Tribal', 'Trip Hop'], 'Folk', ['Alternative Folk', 'Contemporary Folk', 'Folk Rock', 'New Acoustic', 'Traditional Folk', 'World Folk'], 'Inspirational', ['Christian', 'Christian Metal', 'Christian Rap', 'Christian Rock', 'Classic Christian', 'Contemporary Gospel', 'Gospel', 'Southern Gospel', 'Traditional Gospel'], 'International', ['African', 'Arabic', 'Asian', 'Bollywood', 'Brazilian', 'Caribbean', 'Celtic', 'Chinese', 'European', 'Filipino', 'French', 'Greek', 'Hindi', 'Indian', 'Japanese', 'Jewish', 'Klezmer', 'Korean', 'Mediterranean', 'Middle Eastern', 'North American', 'Russian', 'Soca', 'South American', 'Tamil', 'Worldbeat', 'Zouk'], 'Jazz', ['Acid Jazz', 'Avant Garde', 'Big Band', 'Bop', 'Classic Jazz', 'Cool Jazz', 'Fusion', 'Hard Bop', 'Latin Jazz', 'Smooth Jazz', 'Swing', 'Vocal Jazz', 'World Fusion'], 'Latin', ['Bachata', 'Banda', 'Bossa Nova', 'Cumbia', 'Latin Dance', 'Latin Pop', 'Latin Rock', 'Mariachi', 'Merengue', 'Ranchera', 'Reggaeton', 'Regional Mexican', 'Salsa', 'Tango', 'Tejano', 'Tropicalia'], 'Metal', ['Black Metal', 'Classic Metal', 'Extreme Metal', 'Grindcore', 'Hair Metal', 'Heavy Metal', 'Metalcore', 'Power Metal', 'Progressive Metal', 'Rap Metal'], 'Misc', [], 'New Age', ['Environmental', 'Ethnic Fusion', 'Healing', 'Meditation', 'Spiritual'], 'Pop', ['Adult Contemporary', 'Barbershop', 'Bubblegum Pop', 'Dance Pop', 'Idols', 'JPOP', 'Oldies', 'Soft Rock', 'Teen Pop', 'Top 40', 'World Pop'], 'Public Radio', ['College', 'News', 'Sports', 'Talk'], 'Rap', ['Alternative Rap', 'Dirty South', 'East Coast Rap', 'Freestyle', 'Gangsta Rap', 'Hip Hop', 'Mixtapes', 'Old School', 'Turntablism', 'West Coast Rap'], 'Reggae', ['Contemporary Reggae', 'Dancehall', 'Dub', 'Ragga', 'Reggae Roots', 'Rock Steady'], 'Rock', ['Adult Album Alternative', 'British Invasion', 'Classic Rock', 'Garage Rock', 'Glam', 'Hard Rock', 'Jam Bands', 'Piano Rock', 'Prog Rock', 'Psychedelic', 'Rockabilly', 'Surf'], 'Soundtracks', ['Anime', 'Kids', 'Original Score', 'Showtunes', 'Video Game Music'], 'Talk', ['BlogTalk', 'Comedy', 'Community', 'Educational', 'Government', 'News', 'Old Time Radio', 'Other Talk', 'Political', 'Scanner', 'Spoken Word', 'Sports', 'Technology'], 'Themes', ['Adult', 'Best Of', 'Chill', 'Eclectic', 'Experimental', 'Female', 'Heartache', 'Instrumental', 'LGBT', 'Party Mix', 'Patriotic', 'Rainy Day Mix', 'Reality', 'Sexy', 'Shuffle', 'Travel Mix', 'Tribute', 'Trippy', 'Work Mix']]
+ #["default", [], 'TopTen', [], 'Alternative', ['College', 'Emo', 'Hardcore', 'Industrial', 'Punk', 'Ska'], 'Americana', ['Bluegrass', 'Blues', 'Cajun', 'Folk'], 'Classical', ['Contemporary', 'Opera', 'Symphonic'], 'Country', ['Bluegrass', 'New Country', 'Western Swing'], 'Electronic', ['Acid Jazz', 'Ambient', 'Breakbeat', 'Downtempo', 'Drum and Bass', 'House', 'Trance', 'Techno'], 'Hip Hop', ['Alternative', 'Hardcore', 'New School', 'Old School', 'Turntablism'], 'Jazz', ['Acid Jazz', 'Big Band', 'Classic', 'Latin', 'Smooth', 'Swing'], 'Pop/Rock', ['70s', '80s', 'Classic', 'Metal', 'Oldies', 'Pop', 'Rock', 'Top 40'], 'R&B/Soul', ['Classic', 'Contemporary', 'Funk', 'Smooth', 'Urban'], 'Spiritual', ['Alternative', 'Country', 'Gospel', 'Pop', 'Rock'], 'Spoken', ['Comedy', 'Spoken Word', 'Talk'], 'World', ['African', 'Asian', 'European', 'Latin', 'Middle Eastern', 'Reggae'], 'Other/Mixed', ['Eclectic', 'Film', 'Instrumental']]
+ current = ""
+ default = "Alternative"
+ empty = ""
+
+
+ # redefine
+ streams = {}
+
+
+ # extracts the category list from shoutcast.com,
+ # sub-categories are queried per 'AJAX'
+ def update_categories(self):
+ html = http.get(self.base_url)
+ self.categories = ["default"]
+ __print__( html )
+
+ # Radio Genres
+ rx_main = re.compile(r'[\w\s]+', re.S)
+ rx_sub = re.compile(r'