Index: ahttp.py ================================================================== --- ahttp.py +++ ahttp.py @@ -4,20 +4,20 @@ # type: functions # title: http download / methods # description: http utility # version: 1.4 # -# Provides a http GET method with gtk.statusbar() callback. -# And a function to add trailings slashes on http URLs. +# Utility code for HTTP requests, used by all channel plugins. # -# +# Provides a http "GET" method, but also does POST and AJAX- +# simulating requests too. Hooks into mains gtk.statusbar(). +# And can normalize URLs to always carry a trailing slash +# after the domain name. from config import conf, __print__, dbg import requests - - #-- hooks to progress meter and status bar in main window feedback = None @@ -35,23 +35,20 @@ if feedback: try: [feedback(d) for d in args] except: pass - - # prepare default query object session = requests.Session() # 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", + "User-Agent": "streamtuner2/2.1 (X11; Linux amd64; rv:33.0) like WinAmp/2.1", "Accept": "*/*", "Accept-Language": "en-US,en,de,es,fr,it,*;q=0.1", "Accept-Encoding": "gzip, deflate", "Accept-Charset": "UTF-8, ISO-8859-1;q=0.5, *;q=0.1", }) - #-- Retrieve data via HTTP # # Well, it says "get", but it actually does POST and AJAXish GET requests too. @@ -90,12 +87,10 @@ return r.content else: return r.text - - #-- Append missing trailing slash to URLs def fix_url(url): if url is None: url = "" if len(url): @@ -106,9 +101,6 @@ url = "http://" + url # add mandatory path if (url.find("/", 10) < 0): url = url + "/" return url - - - Index: channels/__init__.py ================================================================== --- channels/__init__.py +++ channels/__init__.py @@ -1,6 +1,5 @@ -# # encoding: UTF-8 # api: streamtuner2 # type: base # category: ui # title: Channel plugins @@ -502,90 +501,83 @@ class ChannelPlugin(GenericChannel): module = "abstract" def gui(self, parent): - if parent: - module = self.__class__.__name__ - # two panes - vbox = gtk.HPaned() - vbox.show() - # category treeview - sw1 = gtk.ScrolledWindow() - sw1.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - sw1.set_property("width_request", 150) - sw1.show() - tv1 = gtk.TreeView() - tv1.set_property("width_request", 75) - tv1.set_property("enable_tree_lines", True) - tv1.connect("button_release_event", parent.on_category_clicked) - tv1.show() - sw1.add(tv1) - vbox.pack1(sw1, resize=False, shrink=True) - # stream list - sw2 = gtk.ScrolledWindow() - sw2.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - sw2.show() - tv2 = gtk.TreeView() - tv2.set_property("width_request", 200) - tv2.set_property("enable_tree_lines", True) - tv2.connect("row_activated", parent.on_play_clicked) - tv2.show() - sw2.add(tv2) - vbox.pack2(sw2, resize=True, shrink=True) - - # prepare label - pixbuf = None - if "png" in self.meta: - pixbuf = uikit.pixbuf(self.meta["png"]) - else: - png = get_data("channels/" + self.module + ".png") - pixbuf = uikit.pixbuf(png) - if pixbuf: - icon = gtk.image_new_from_pixbuf(pixbuf) - else: - icon = gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, size=1) - label = gtk.HBox() - label.pack_start(icon, expand=False, fill=True) - label.pack_start(gtk.Label(self.meta.get("title", self.module)), expand=True, fill=True) - - # pack it into an event container to catch double-clicks - ev_label = gtk.EventBox() - ev_label.add(label) - ev_label.connect('event', parent.on_homepage_channel_clicked) - plain_label = gtk.Label(self.module) - - - - # to widgets - self.gtk_cat = tv1 - parent.widgets[module + "_cat"] = tv1 - self.gtk_list = tv2 - parent.widgets[module + "_list"] = tv2 - ev_label.show_all() - vbox.show_all() - parent.widgets["v_" + module] = vbox - parent.widgets["c_" + module] = ev_label - tv2.connect('button-press-event', parent.station_context_menu) - - - # try to initialize superclass now, before adding to channel tabs - GenericChannel.gui(self, parent) - - - # add notebook tab - tab = parent.notebook_channels.insert_page_menu(vbox, ev_label, plain_label, -1) - - - - # double-click catch - - - # add module to list - #parent.channels[module] = None - #parent.channel_names.append(module) - """ -> already taken care of in main.load_plugins() """ - - - - + + if not parent: + return + + module = self.__class__.__name__ + # two panes + vbox = gtk.HPaned() + vbox.show() + # category treeview + sw1 = gtk.ScrolledWindow() + sw1.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw1.set_property("width_request", 150) + sw1.show() + tv1 = gtk.TreeView() + tv1.set_property("width_request", 75) + tv1.set_property("enable_tree_lines", True) + tv1.connect("button_release_event", parent.on_category_clicked) + tv1.show() + sw1.add(tv1) + vbox.pack1(sw1, resize=False, shrink=True) + # stream list + sw2 = gtk.ScrolledWindow() + sw2.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw2.show() + tv2 = gtk.TreeView() + tv2.set_property("width_request", 200) + tv2.set_property("enable_tree_lines", True) + tv2.connect("row_activated", parent.on_play_clicked) + tv2.show() + sw2.add(tv2) + vbox.pack2(sw2, resize=True, shrink=True) + + # prepare label + pixbuf = None + if "png" in self.meta: + pixbuf = uikit.pixbuf(self.meta["png"]) + else: + png = get_data("channels/" + self.module + ".png") + pixbuf = uikit.pixbuf(png) + if pixbuf: + icon = gtk.image_new_from_pixbuf(pixbuf) + else: + icon = gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, size=1) + label = gtk.HBox() + label.pack_start(icon, expand=False, fill=True) + label.pack_start(gtk.Label(self.meta.get("title", self.module)), expand=True, fill=True) + + # pack it into an event container to catch double-clicks + ev_label = gtk.EventBox() + ev_label.add(label) + ev_label.connect('event', parent.on_homepage_channel_clicked) + plain_label = gtk.Label(self.module) + + # to widgets + self.gtk_cat = tv1 + parent.widgets[module + "_cat"] = tv1 + self.gtk_list = tv2 + parent.widgets[module + "_list"] = tv2 + ev_label.show_all() + vbox.show_all() + parent.widgets["v_" + module] = vbox + parent.widgets["c_" + module] = ev_label + tv2.connect('button-press-event', parent.station_context_menu) + + + # try to initialize superclass now, before adding to channel tabs + GenericChannel.gui(self, parent) + + # add notebook tab + tab = parent.notebook_channels.insert_page_menu(vbox, ev_label, plain_label, -1) + + # double-click catch + + # add module to list + #parent.channels[module] = None + #parent.channel_names.append(module) + """ -> already taken care of in main.load_plugins() """ Index: channels/bookmarks.py ================================================================== --- channels/bookmarks.py +++ channels/bookmarks.py @@ -13,12 +13,11 @@ # This module lists static content from ~/.config/streamtuner2/bookmarks.json. # Any bookmarked station will appear with a star ★ icon in other channels. # # Some feature extensions inject custom subcategories here. For example the # "search" feature adds its own result list here, as does the "timer" plugin. -# -# + from config import * from uikit import uikit from channels import * @@ -34,180 +33,177 @@ # # It's accessible as `main.bookmarks` in the ST2 window and elsewhere. # class bookmarks(GenericChannel): - # desc - module = "bookmarks" - title = "bookmarks" - base_url = "file:.config/streamtuner2/bookmarks.json" - listformat = "*/*" - - # content - categories = ["favourite", ] # timer, links, search, and links show up as needed - current = "favourite" - default = "favourite" - streams = {"favourite":[], "search":[], "scripts":[], "timer":[], "history":[], } - - - # cache list, to determine if a PLS url is bookmarked - urls = [] - - - - def gui(self, parent): - GenericChannel.gui(self, parent) - parent.notebook_channels.set_menu_label_text(parent.v_bookmarks, "bookmarks") - - - # this channel does not actually retrieve/parse data from anywhere - def update_categories(self): - pass - - # but category sub-plugins might provide a hook - category_plugins = {} - def update_streams(self, cat): - if cat in self.category_plugins: - return self.category_plugins[cat].update_streams(cat) or [] - else: - return self.streams.get(cat, []) - - - # streams are already loaded at instantiation - def first_show(self): - pass - - - # all entries just come from "bookmarks.json" - def cache(self): - # stream list - cache = conf.load(self.module) - if (cache): - __print__(dbg.PROC, "load bookmarks.json") - self.streams = cache - - - - # save to cache file - def save(self): - conf.save(self.module, self.streams, nice=1) - - - # checks for existence of an URL in bookmarks store, - # this method is called by other channel modules' display() method - def is_in(self, url, once=1): - if (not self.urls): - self.urls = [row.get("url","urn:x-streamtuner2:no") for row in self.streams["favourite"]] - return url in self.urls - - - # called from main window / menu / context menu, - # when bookmark is to be added for a selected stream entry - def add(self, row): - - # normalize data (this row originated in a gtk+ widget) - row["favourite"] = 1 - if row.get("favicon"): - row["favicon"] = favicon.file(row.get("homepage")) - if not row.get("listformat"): - row["listformat"] = main.channel().listformat - - # append to storage - self.streams["favourite"].append(row) - self.save() - self.load(self.default) - self.urls.append(row["url"]) - - - # simplified gtk TreeStore display logic (just one category for the moment, always rebuilt) - def load(self, category, force=False): - __print__(dbg.UI, category, self.streams.keys()) - self.streams[category] = self.update_streams(category) - #self.liststore[category] = \ - uikit.columns(self.gtk_list, self.datamap, self.prepare(self.streams[category])) - - - # add a categories[]/streams{} subcategory, update treeview - def add_category(self, cat, plugin=None): - if cat not in self.categories: # add category if missing - self.categories.append(cat) - self.display_categories() - if cat not in self.streams: - self.streams[cat] = [] - if plugin: - self.category_plugins[cat] = plugin - - - # change cursor - def set_category(self, cat): - self.add_category(cat) - self.gtk_cat.get_selection().select_path(str(self.categories.index(cat))) - return self.currentcat() - - - # update bookmarks from freshly loaded streams data - def heuristic_update(self, updated_channel, updated_category): - - if not conf.heuristic_bookmark_update: return - __print__(dbg.ERR, "heuristic bookmark update") - save = 0 - fav = self.streams["favourite"] - - # First we'll generate a list of current bookmark stream urls, and then - # remove all but those from the currently UPDATED_channel + category. - # This step is most likely redundant, but prevents accidently re-rewriting - # stations that are in two channels (=duplicates with different PLS urls). - check = {"http//": "[row]"} - check = dict((row.get("url", "http//"),row) for row in fav) - # walk through all channels/streams - for chname,channel in main.channels.items(): - for cat,streams in channel.streams.items(): - - # keep the potentially changed rows - if (chname == updated_channel) and (cat == updated_category): - freshened_streams = streams - - # remove unchanged urls/rows - else: - unchanged_urls = (row.get("url") for row in streams) - for url in unchanged_urls: - if url in check: - del check[url] - # directory duplicates could unset the check list here, - # so we later end up doing a deep comparison - - - # now the real comparison, - # where we compare station titles and homepage url to detect if a bookmark is an old entry - for row in freshened_streams: - url = row.get("url") - - # empty entry (google stations), or stream still in current favourites - if not url or url in check: - pass - - # need to search - else: - title = row.get("title") - homepage = row.get("homepage") - for i,old in enumerate(fav): - - # skip if new url already in streams - if url == old.get("url"): - pass # This is caused by channel duplicates with identical PLS links. - - # on exact matches (but skip if url is identical anyway) - elif title == old["title"] and homepage == old.get("homepage",homepage): - # update stream url - fav[i]["url"] = url - save = 1 - - # more text similarity heuristics might go here - else: - pass - - # if there were changes - if save: self.save() - - - +# desc +module = "bookmarks" +title = "bookmarks" +base_url = "file:.config/streamtuner2/bookmarks.json" +listformat = "*/*" + +# content +categories = ["favourite", ] # timer, links, search, and links show up as needed +current = "favourite" +default = "favourite" +streams = {"favourite":[], "search":[], "scripts":[], "timer":[], "history":[], } + + +# cache list, to determine if a PLS url is bookmarked +urls = [] + + + +def gui(self, parent): + GenericChannel.gui(self, parent) + parent.notebook_channels.set_menu_label_text(parent.v_bookmarks, "bookmarks") + + +# this channel does not actually retrieve/parse data from anywhere +def update_categories(self): + pass + +# but category sub-plugins might provide a hook +category_plugins = {} +def update_streams(self, cat): + if cat in self.category_plugins: + return self.category_plugins[cat].update_streams(cat) or [] + else: + return self.streams.get(cat, []) + + +# streams are already loaded at instantiation +def first_show(self): + pass + + +# all entries just come from "bookmarks.json" +def cache(self): + # stream list + cache = conf.load(self.module) + if (cache): + __print__(dbg.PROC, "load bookmarks.json") + self.streams = cache + + + +# save to cache file +def save(self): + conf.save(self.module, self.streams, nice=1) + + +# checks for existence of an URL in bookmarks store, +# this method is called by other channel modules' display() method +def is_in(self, url, once=1): + if (not self.urls): + self.urls = [row.get("url","urn:x-streamtuner2:no") for row in self.streams["favourite"]] + return url in self.urls + + +# called from main window / menu / context menu, +# when bookmark is to be added for a selected stream entry +def add(self, row): + + # normalize data (this row originated in a gtk+ widget) + row["favourite"] = 1 + if row.get("favicon"): + row["favicon"] = favicon.file(row.get("homepage")) + if not row.get("listformat"): + row["listformat"] = main.channel().listformat + + # append to storage + self.streams["favourite"].append(row) + self.save() + self.load(self.default) + self.urls.append(row["url"]) + + +# simplified gtk TreeStore display logic (just one category for the moment, always rebuilt) +def load(self, category, force=False): + __print__(dbg.UI, category, self.streams.keys()) + self.streams[category] = self.update_streams(category) + #self.liststore[category] = \ + uikit.columns(self.gtk_list, self.datamap, self.prepare(self.streams[category])) + + +# add a categories[]/streams{} subcategory, update treeview +def add_category(self, cat, plugin=None): + if cat not in self.categories: # add category if missing + self.categories.append(cat) + self.display_categories() + if cat not in self.streams: + self.streams[cat] = [] + if plugin: + self.category_plugins[cat] = plugin + + +# change cursor +def set_category(self, cat): + self.add_category(cat) + self.gtk_cat.get_selection().select_path(str(self.categories.index(cat))) + return self.currentcat() + + +# update bookmarks from freshly loaded streams data +def heuristic_update(self, updated_channel, updated_category): + + if not conf.heuristic_bookmark_update: return + __print__(dbg.ERR, "heuristic bookmark update") + save = 0 + fav = self.streams["favourite"] + + # First we'll generate a list of current bookmark stream urls, and then + # remove all but those from the currently UPDATED_channel + category. + # This step is most likely redundant, but prevents accidently re-rewriting + # stations that are in two channels (=duplicates with different PLS urls). + check = {"http//": "[row]"} + check = dict((row.get("url", "http//"),row) for row in fav) + # walk through all channels/streams + for chname,channel in main.channels.items(): + for cat,streams in channel.streams.items(): + + # keep the potentially changed rows + if (chname == updated_channel) and (cat == updated_category): + freshened_streams = streams + + # remove unchanged urls/rows + else: + unchanged_urls = (row.get("url") for row in streams) + for url in unchanged_urls: + if url in check: + del check[url] + # directory duplicates could unset the check list here, + # so we later end up doing a deep comparison + + + # now the real comparison, + # where we compare station titles and homepage url to detect if a bookmark is an old entry + for row in freshened_streams: + url = row.get("url") + + # empty entry (google stations), or stream still in current favourites + if not url or url in check: + pass + + # need to search + else: + title = row.get("title") + homepage = row.get("homepage") + for i,old in enumerate(fav): + + # skip if new url already in streams + if url == old.get("url"): + pass # This is caused by channel duplicates with identical PLS links. + + # on exact matches (but skip if url is identical anyway) + elif title == old["title"] and homepage == old.get("homepage",homepage): + # update stream url + fav[i]["url"] = url + save = 1 + + # more text similarity heuristics might go here + else: + pass + + # if there were changes + if save: self.save() Index: channels/file.py ================================================================== --- channels/file.py +++ channels/file.py @@ -1,6 +1,5 @@ -# # api: streamtuner2 # title: File browser # description: Displays mp3/oggs or m3u/pls files from local media file directories. # type: channel # category: local @@ -9,12 +8,11 @@ # depends: mutagen # config: # { name: file_browser_dir, type: text, value: "~/Music, /media/music", description: "List of directories to scan for audio files." }, # { name: file_browser_ext, type: text, value: "mp3,ogg, m3u,pls,xspf, avi,flv,mpg,mp4", description: "File type/extension filter." }, # -# Local file browser. -# +# Local file browser. Presents files from configured directories. # modules import os @@ -121,10 +119,12 @@ def scan_dirs(self): self.categories = [] # add main directory for main in self.dir: + main = re.sub("^~", os.environ.get("HOME")) + main = re.sub("[$]([A-Z_]+)", lambda m: os.environ.get(m.group(1)), main) if os.path.exists(main): self.categories.append(main) # prepare subdirectories list sub = [] Index: channels/history.py ================================================================== --- channels/history.py +++ channels/history.py @@ -7,20 +7,16 @@ # category: ui # config: # { name: history, type: int, value: 20, description: Number of last played streams to keep in history list., category: limit } # priority: optional # -# -# Lists last activated streams in a new [history] tab in the favourites -# channel. -# - +# Lists recently played streams in a new [history] tab in the +# bookmarks channel. from config import * from channels import * - class history: # plugin info Index: channels/icast.py ================================================================== --- channels/icast.py +++ channels/icast.py @@ -17,22 +17,21 @@ # AwAAAAFiS0dEBxZhiOsAAAAJcEhZcwAAAEgAAABIAEbJaz4AAADxSURBVBjTY2AAAUYmIGBkZoACRhZWNnZ2Dk4uqAgjNw8vH7+AoJCwCCOIzywqJi4hKSUtIysnrwBSo6ikrKKqpq6hqaWto6QIVKCr # p69uYGBoZGxiYmrGyczAaG5haWVtbW1ja2fv4GjOyMDo5Ozi6ubu4enl7ePr5w8U8A8IDAoOCQ0Lj4iMigYJmDtHRcbExsUnJCYlR6YwMjCzpqalZ2SGZ2Vn5+TmsTIzMOcXFBYVl5TmZGeXlXPkgxxa # UZleVV1TW1efyNYAdmpjU3NLq1Zbe0dnVzfEM0w9vX3N/RMmTmqEeZd58pSpU6dNBnsWAEP5Nco3FJZfAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE0LTA2LTAxVDAxOjI4OjA3KzAyOjAw7O+A+AAAACV0 # RVh0ZGF0ZTptb2RpZnkAMjAxNC0wNi0wMVQwMToyODowNyswMjowMJ2yOEQAAAAASUVORK5CYII= # 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 only 10 entries # per query results make fetching slow. -# -# -# + import re import json from config import conf, dbg, __print__ from channels import * Index: channels/internet_radio.py ================================================================== --- channels/internet_radio.py +++ channels/internet_radio.py @@ -1,6 +1,5 @@ -# # api: streamtuner2 # title: Internet-Radio # description: Broad list of webradios from all genres. # type: channel # category: radio @@ -8,23 +7,18 @@ # url: http://www.internet-radio.org.uk/ # config: # { name: internetradio_max_pages, type: int, value: 5, category: limit, description: How many pages to fetch and read. } # priority: standard # -# Internet-Radio.co.uk/.com is one of the largest directories of streams. +# Internet-Radio.co.uk/.com is one of the largest stream directories. # Available music genre classifications are mirrored verbatim and flatly. # # The new version of this plugin alternates between PyQuery and Regex # station extraction. Both overlook some paid or incomplete entries. # HTTP retrieval happens in one batch, determined by the number of pages # setting, rather than the global max_streams option. # -# -# -# -# - from channels import * import re from config import conf, __print__, dbg Index: channels/itunes.py ================================================================== --- channels/itunes.py +++ channels/itunes.py @@ -8,11 +8,13 @@ # url: http://www.itunes.com? # priority: optional # config: - # documentation: http://lab.rolisoft.net/playlists.html # -# Provides pre-parsed radio station playlists for various services +# +# API provides pre-parsed radio station playlists for various services +# # → Shoutcast # → Xiph/ICEcast # → Tunein # → iTunes # → FilterMusic @@ -19,11 +21,10 @@ # → SomaFM # → AccuRadio # → BBC # # In this module only iTunes will be queried for now. -# # import re from config import conf, dbg, __print__ from channels import * Index: channels/jamendo.py ================================================================== --- channels/jamendo.py +++ channels/jamendo.py @@ -21,10 +21,20 @@ # Tracks are queried by genre, where currently there's just a small built-in # tag list in ST2 # # Per default Ogg Vorbis is used as streaming format. Playlists and albums # return as XSPF playlists. + + +import re +import ahttp as http +from config import conf, __print__, dbg +from channels import * +import json + + +# jamendo CC music sharing site # # # The v3.0 streaming URLs don't seem to work. Therefore some /get2 URLs will # be used. # @@ -33,25 +43,10 @@ # [+] http://api.jamendo.com/get2/stream/track/xspf/?playlist_id=171574&n=all&order=random # [+] http://api.jamendo.com/get2/stream/track/xspf/?album_id=%s&streamencoding=ogg2&n=all # # Seem to resolve to OGG Vorbis each. # - - - -import re -import ahttp as http -from config import conf, __print__, dbg -from channels import * -import json - - - - - - -# jamendo CC music sharing site class jamendo (ChannelPlugin): # description title = "Jamendo" module = "jamendo" Index: channels/live365.py ================================================================== --- channels/live365.py +++ channels/live365.py @@ -7,19 +7,13 @@ # category: radio # url: http://www.live365.com/ # config: - # priority: optional # -# -# 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 -# +# Live365 lists around 5000 radio stations. Some are paid +# entries and require a logon. This plugins tries to filter +# thoise out. # streamtuner2 modules from config import conf from uikit import uikit @@ -39,10 +33,19 @@ from time import time from xml.dom.minidom import parseString # channel live365 +# +# 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 +# class live365(ChannelPlugin): # desc module = "live365" title = "Live365" Index: channels/modarchive.py ================================================================== --- channels/modarchive.py +++ channels/modarchive.py @@ -7,33 +7,32 @@ # url: http://www.modarchive.org/ # priority: extra # config: - # category: collection # -# -# Just a genre browser. +# A genre browser for tracker music files from the MOD Archive. # # 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. # -# Modarchive actually provides an API -# http://modarchive.org/index.php?xml-api -# (If only it wasn't XML based..) -# +# Configuring VLC for */* is the best option. See the help on how to +# setup wget/curl to download them. import re import ahttp as http from config import conf from channels import * from config import __print__, dbg - - -# MODs +# The MOD Archive +# +# Modarchive actually provides an API +# http://modarchive.org/index.php?xml-api +# (If only it wasn't XML based..) +# class modarchive (ChannelPlugin): # description title = "modarchive" module = "modarchive" Index: channels/myoggradio.py ================================================================== --- channels/myoggradio.py +++ channels/myoggradio.py @@ -1,6 +1,6 @@ -# + # api: streamtuner2 # title: MyOggRadio # description: Open source internet radio directory. # type: channel # category: radio @@ -18,11 +18,10 @@ # 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 @@ -179,9 +178,8 @@ else: lap = conf.netrc(["myoggradio", "myoggradio.org", "www.myoggradio.org"]) if lap: return [lap[0] or lap[1], lap[2]] pass - Index: channels/search.py ================================================================== --- channels/search.py +++ channels/search.py @@ -9,10 +9,13 @@ # # Configuration dialog for audio applications, # general settings, and plugin activation and # associated options. # +# Some plugins hook into the saving method. Most +# require a restart of streamtuner2 for changes +# to take effect. from uikit import * import channels from config import * Index: channels/shoutcast.py ================================================================== --- channels/shoutcast.py +++ channels/shoutcast.py @@ -12,15 +12,29 @@ # priority: default # depends: pq, re, http # # Shoutcast is a server software for audio streaming. It automatically spools # 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. -# -# http://wiki.winamp.com/wiki/SHOUTcast_Radio_Directory_API +# took place. The former yellow pages API got deprecated. + + +import ahttp as http +from json import loads as json_decode +import re +from config import conf, __print__, dbg +from pq import pq +from channels import * # works everywhere but in this plugin(???!) +import channels +from compat2and3 import urllib + + + +# SHOUTcast data module # +# Former API doc: 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: @@ -28,25 +42,10 @@ # 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 -#from channels import * # works everywhere but in this plugin(???!) -import channels -from compat2and3 import urllib - - - -# SHOUTcast data module ---------------------------------------- class shoutcast(channels.ChannelPlugin): # desc module = "shoutcast" title = "SHOUTcast" Index: channels/tunein.py ================================================================== --- channels/tunein.py +++ channels/tunein.py @@ -15,11 +15,10 @@ # # Only radio listings are queried for now. But there are # heaps more talk and local show entries, etc. (Would require # more deeply nested category tree.) # - import re import json from config import conf, dbg, __print__ Index: channels/xiph.py ================================================================== --- channels/xiph.py +++ channels/xiph.py @@ -8,504 +8,504 @@ # category: radio # config: # { name: xiph_min_bitrate, value: 64, type: int, description: "minimum bitrate, filter anything below", category: filter } # priority: standard # -# Xiph.org maintains the Ogg streaming standard and Vorbis audio compression -# format, amongst others. The ICEcast server is an alternative to SHOUTcast. +# Xiph.org maintains the Ogg streaming standard and Vorbis +# audio compression format, amongst others. The ICEcast +# server is an alternative to SHOUTcast. +# +# It also provides a directory listing of known internet +# radio stations, only a handful of them using Ogg though. +# +# The category list is hardwired in this plugin. +# + + +from config import * +from uikit import uikit +import ahttp as http +from channels import * +#from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities +#import xml.dom.minidom +import json +import re + + + +# Xiph via I-O +# # -# It meanwhile provides a JSOL dump, which is faster to download and process. +# Xiph meanwhile provides a JSOL dump, which is faster to download and process. # So we'll use that over the older yp.xml. (Sadly it also doesn't output # homepage URLs, listeners, etc.) # # Xiphs JSON is a horrible mysqldump concatenation, not parseable. Thus it's -# refurbished on api.io for consumption. Which also provides compressed HTTP -# transfers and category slicing. +# refurbished on //api.include-once.org/xiph/cache.php for consumption. Which +# also provides compressed HTTP transfers and category slicing. # # Xiph won't be updating the directory for another while. The original feature # request is now further delayed as summer of code project: # · https://trac.xiph.org/ticket/1958 # · https://wiki.xiph.org/Summer_of_Code_2015#Stream_directory_API # -# - - - -# streamtuner2 modules -from config import conf -from uikit import uikit -import ahttp as http -from channels import * -from config import __print__, dbg -import json - -# python modules -import re -#from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities -#import xml.dom.minidom - - - - -# I wonder what that is for --------------------------------------- -class xiph (ChannelPlugin): - - # desc - api = "streamtuner2" - module = "xiph" - title = "Xiph.org" - homepage = "http://dir.xiph.org/" - #xml_url = "http://dir.xiph.org/yp.xml" - json_url = "http://api.include-once.org/xiph/cache.php" - listformat = "url/http" - has_search = True - - # content - categories = [ "pop", "top40" ] - current = "" - default = "pop" - empty = None - - - # prepare category names - def __init__(self, parent=None): - - self.categories = [] - self.filter = {} - for main in self.genres: - if (type(main) == str): - id = main.split("|") - self.categories.append(id[0].title()) - self.filter[id[0]] = main - else: - l = [] - for sub in main: - id = sub.split("|") - l.append(id[0].title()) - self.filter[id[0]] = sub - self.categories.append(l) - - # GUI - ChannelPlugin.__init__(self, parent) - - - # just counts genre tokens, does not automatically create a category tree from it - def update_categories(self): - pass - - - # downloads stream list from xiph.org for given category - def update_streams(self, cat, search=None): - - # With the new JSON cache API on I-O, we can load categories individually: - params = {} - if cat: - params["cat"] = cat.lower() - if search: - params["search"] = search - - #-- get data - data = http.get(self.json_url, params=params) - #__print__(dbg.DATA, data) - - #-- extract - l = [] - __print__( dbg.PROC, "processing api.dir.xiph.org JSON (via api.include-once.org cache)" ) - data = json.loads(data) - 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): - 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 - - - - - genres = [ - "pop", - [ - "top40", - "90s", - "80s", - "britpop", - "disco", - "urban", - "party", - "mashup", - "kpop", - "jpop", - "lounge", - "softpop", - "top", - "popular", - "schlager", - ], - "rock", - [ - "alternative", - "electro", - "country", - "mixed", - "metal", - "eclectic", - "folk", - "anime", - "hardcore", - "pure" - "jrock" - ], - "dance", - [ - "electronic", - "deephouse", - "dancefloor", - "elektro" - "eurodance" - "b", - "r", - ], - "hits", - [ - "russian" - "hit", - "star" - ], - "radio", - [ - "live", - "community", - "student", - "internet", - "webradio", - ], - "classic", - [ - "classical", - "ebu", - "vivaldi", - "piano", - "opera", - "classix", - "chopin", - "renaissance", - "classique", - ], - "talk", - [ - "news", - "politics", - "medicine", - "health" - "sport", - "education", - "entertainment", - "podcast", - ], - "various", - [ - "hits", - "ruhit", - "mega" - ], - "house", - [ - "lounge", - "trance", - "techno", - "handsup", - "gay", - "breaks", - "dj", - "electronica", - ], - "trance", - [ - "clubbing", - "electronical" - ], - "jazz", - [ - "contemporary" - ], - "oldies", - [ - "golden", - "decades", - "info", - "70s", - "60s" - ], - "religious", - [ - "spiritual", - "inspirational", - "christian", - "catholic", - "teaching", - "christmas", - "gospel", - ], - "music", - "unspecified", - "misc", - "adult", - "indie", - [ - "reggae", - "blues", - "college", - "soundtrack" - ], - "mixed", - [ - "disco", - "mainstream", - "soulfull" - ], - "funk", - "hiphop", - [ - "rap", - "dubstep", - "hip", - "hop" - ], - "top", - [ - "urban" - ], - "musica", - "ambient", - [ - "downtempo", - "dub" - ], - "promodj", - "world", # REGIONAL - [ - "france", - "greek", - "german", - "westcoast", - "bollywood", - "indian", - "nederlands", - "europa", - "italia", - "brazilian", - "tropical", - "korea", - "seychelles", - "black", - "japanese", - "ethnic", - "country", - "americana", - "western", - "cuba", - "afrique", - "paris", - "celtic", - "ambiance", - "francais", - "liberte", - "anglais", - "arabic", - "hungary", - "folklore" - "latin", - "dutch" - "italy" - ], - "artist", # ARTIST NAMES - [ - "mozart", - "beatles", - "michael", - "nirvana", - "elvis", - "britney", - "abba", - "madonna", - "depeche", - ], - "salsa", - "love", - "la", - "soul", - "techno", - [ - "club", - "progressive", - "deep" - "electro", - ], - "best", - "100%", - "rnb", - "retro", - "new", - "smooth", - [ - "cool" - ], - "easy", - [ - "lovesongs", - "relaxmusic" - ], - "chillout", - "slow", - [ - "soft" - ], - "mix", - [ - "modern" - ], - "punk", - [ - "ska" - ], - "international", - "bass", - "zouk", - "video", - [ - "game" - ], - "hardstyle", - "scanner", - "chill", - [ - "out", - "trip" - ], - "drum", - "roots", - "ac", - [ - "chr", - "dc" - ], - "public", - "contemporary", - [ - "instrumental" - ], - "minimal", - "hot", - [ - "based" - ], - "free", - [ - "format" - ], - "hard", - [ - "heavy", - "classicrock" - ], - "reggaeton", - "southern", - "musica", - "old", - "emisora", - "img", - "rockabilly", - "charts", - [ - "best80", - "70er", - "80er", - "60er" - "chart", - ], - "other", - [ - "varios" - ], - "soulful", - "listening", - "vegyes", - "creative", - "variety", - "commons", - [ - "ccmusik" - ], - "tech", - [ - "edm", - "prog" - ], - "minecraft", - "animes", - "goth", - "technologie", - "tout", - "musical", - [ - "broadway" - ], - "romantica", - "newage", - "nostalgia", - "oldschool", - [ - "00s" - ], - "wij", - "relax", - [ - "age" - ], - "theatre", - "gothic", - "dnb", - "disney", - "funky", - "young", - "psychedelic", - "habbo", - "experimental", - "exitos", - "digital", - "no", - "industrial", - "epic", - "soundtracks", - "cover", - "chd", - "games", - "libre", - "wave", - "vegas", - "comedy", - "alternate", - "instrumental", - [ - "swing" - ], - "ska", - [ - "punkrock", - "oi" - ], - "darkwave", - ] +class xiph (ChannelPlugin): + + # desc + module = "xiph" + title = "Xiph.org" + homepage = "http://dir.xiph.org/" + #xml_url = "http://dir.xiph.org/yp.xml" + json_url = "http://api.include-once.org/xiph/cache.php" + listformat = "url/http" + has_search = True + + # content + categories = [ "pop", "top40" ] + current = "" + default = "pop" + empty = None + + + # prepare category names + def __init__(self, parent=None): + + self.categories = [] + self.filter = {} + for main in self.genres: + if (type(main) == str): + id = main.split("|") + self.categories.append(id[0].title()) + self.filter[id[0]] = main + else: + l = [] + for sub in main: + id = sub.split("|") + l.append(id[0].title()) + self.filter[id[0]] = sub + self.categories.append(l) + + # GUI + ChannelPlugin.__init__(self, parent) + + + # just counts genre tokens, does not automatically create a category tree from it + def update_categories(self): + pass + + + # downloads stream list from xiph.org for given category + def update_streams(self, cat, search=None): + + # With the new JSON cache API on I-O, we can load categories individually: + params = {} + if cat: + params["cat"] = cat.lower() + if search: + params["search"] = search + + #-- get data + data = http.get(self.json_url, params=params) + #__print__(dbg.DATA, data) + + #-- extract + l = [] + __print__( dbg.PROC, "processing api.dir.xiph.org JSON (via api.include-once.org cache)" ) + data = json.loads(data) + 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): + 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 + + + + + genres = [ + "pop", + [ + "top40", + "90s", + "80s", + "britpop", + "disco", + "urban", + "party", + "mashup", + "kpop", + "jpop", + "lounge", + "softpop", + "top", + "popular", + "schlager", + ], + "rock", + [ + "alternative", + "electro", + "country", + "mixed", + "metal", + "eclectic", + "folk", + "anime", + "hardcore", + "pure" + "jrock" + ], + "dance", + [ + "electronic", + "deephouse", + "dancefloor", + "elektro" + "eurodance" + "b", + "r", + ], + "hits", + [ + "russian" + "hit", + "star" + ], + "radio", + [ + "live", + "community", + "student", + "internet", + "webradio", + ], + "classic", + [ + "classical", + "ebu", + "vivaldi", + "piano", + "opera", + "classix", + "chopin", + "renaissance", + "classique", + ], + "talk", + [ + "news", + "politics", + "medicine", + "health" + "sport", + "education", + "entertainment", + "podcast", + ], + "various", + [ + "hits", + "ruhit", + "mega" + ], + "house", + [ + "lounge", + "trance", + "techno", + "handsup", + "gay", + "breaks", + "dj", + "electronica", + ], + "trance", + [ + "clubbing", + "electronical" + ], + "jazz", + [ + "contemporary" + ], + "oldies", + [ + "golden", + "decades", + "info", + "70s", + "60s" + ], + "religious", + [ + "spiritual", + "inspirational", + "christian", + "catholic", + "teaching", + "christmas", + "gospel", + ], + "music", + "unspecified", + "misc", + "adult", + "indie", + [ + "reggae", + "blues", + "college", + "soundtrack" + ], + "mixed", + [ + "disco", + "mainstream", + "soulfull" + ], + "funk", + "hiphop", + [ + "rap", + "dubstep", + "hip", + "hop" + ], + "top", + [ + "urban" + ], + "musica", + "ambient", + [ + "downtempo", + "dub" + ], + "promodj", + "world", # REGIONAL + [ + "france", + "greek", + "german", + "westcoast", + "bollywood", + "indian", + "nederlands", + "europa", + "italia", + "brazilian", + "tropical", + "korea", + "seychelles", + "black", + "japanese", + "ethnic", + "country", + "americana", + "western", + "cuba", + "afrique", + "paris", + "celtic", + "ambiance", + "francais", + "liberte", + "anglais", + "arabic", + "hungary", + "folklore" + "latin", + "dutch" + "italy" + ], + "artist", # ARTIST NAMES + [ + "mozart", + "beatles", + "michael", + "nirvana", + "elvis", + "britney", + "abba", + "madonna", + "depeche", + ], + "salsa", + "love", + "la", + "soul", + "techno", + [ + "club", + "progressive", + "deep" + "electro", + ], + "best", + "100%", + "rnb", + "retro", + "new", + "smooth", + [ + "cool" + ], + "easy", + [ + "lovesongs", + "relaxmusic" + ], + "chillout", + "slow", + [ + "soft" + ], + "mix", + [ + "modern" + ], + "punk", + [ + "ska" + ], + "international", + "bass", + "zouk", + "video", + [ + "game" + ], + "hardstyle", + "scanner", + "chill", + [ + "out", + "trip" + ], + "drum", + "roots", + "ac", + [ + "chr", + "dc" + ], + "public", + "contemporary", + [ + "instrumental" + ], + "minimal", + "hot", + [ + "based" + ], + "free", + [ + "format" + ], + "hard", + [ + "heavy", + "classicrock" + ], + "reggaeton", + "southern", + "musica", + "old", + "emisora", + "img", + "rockabilly", + "charts", + [ + "best80", + "70er", + "80er", + "60er" + "chart", + ], + "other", + [ + "varios" + ], + "soulful", + "listening", + "vegyes", + "creative", + "variety", + "commons", + [ + "ccmusik" + ], + "tech", + [ + "edm", + "prog" + ], + "minecraft", + "animes", + "goth", + "technologie", + "tout", + "musical", + [ + "broadway" + ], + "romantica", + "newage", + "nostalgia", + "oldschool", + [ + "00s" + ], + "wij", + "relax", + [ + "age" + ], + "theatre", + "gothic", + "dnb", + "disney", + "funky", + "young", + "psychedelic", + "habbo", + "experimental", + "exitos", + "digital", + "no", + "industrial", + "epic", + "soundtracks", + "cover", + "chd", + "games", + "libre", + "wave", + "vegas", + "comedy", + "alternate", + "instrumental", + [ + "swing" + ], + "ska", + [ + "punkrock", + "oi" + ], + "darkwave", + ] Index: channels/youtube.py ================================================================== --- channels/youtube.py +++ channels/youtube.py @@ -43,11 +43,10 @@ # get hidden in the result sets. # Google uses some quote/billing algorithm for all queries. It seems # sufficient for Streamtuner2 for now, as the fields= JSON filter strips # a lot of uneeded data. (Clever idea, but probably incurs more processing # effort on Googles servers than it actually saves bandwidth, but hey..) -# # # EXAMPLES # # api("videos", chart="mostPopular") # api("search", chart="mostPopular", videoCategoryId=10, order="date", type="video")