Internet radio browser GUI for music/video streams from various directory services.

โŒˆโŒ‹ โŽ‡ branch:  streamtuner2


Check-in [f5a59de6b8]

Overview
Comment:Compact plugin comments to be more user-targetted.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1:f5a59de6b8ebfc416e7dc892b23eee9658061c25
User & Date: mario on 2015-04-02 17:38:08
Other Links: manifest | tags
Context
2015-04-02
18:40
Fix incorrent indendation. check-in: 85b88fac15 user: mario tags: trunk
17:38
Compact plugin comments to be more user-targetted. check-in: f5a59de6b8 user: mario tags: trunk
15:50
Initial version of Radio-Browser.info API plugin. Built-in stub favicon. (Maybe later support station submissions here too, just like MOR.) check-in: 8fcde37560 user: mario tags: trunk
Changes

Modified ahttp.py from [1df75bd959] to [20908d5cb6].

2
3
4
5
6
7
8
9
10
11

12


13
14
15
16
17
18
19
20
21
22
23
24
25
..
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
..
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
...
104
105
106
107
108
109
110
111
112
113
114
# encoding: UTF-8
# api: streamtuner2
# 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.
#

#




from config import conf, __print__, dbg
import requests




#-- hooks to progress meter and status bar in main window
feedback = None

# Sets either text or percentage of main windows' status bar.
#
................................................................................

  # send to main win
  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",
    "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.
#
def get(url, params={}, referer="", post=0, ajax=0, binary=0, feedback=None, content=True):
................................................................................
        return r
    elif binary:
        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):
        # remove whitespace
        url = url.strip()
................................................................................
        # add scheme
        if (url.find("://") < 0):
            url = "http://" + url
        # add mandatory path
        if (url.find("/", 10) < 0):
            url = url + "/"
    return url











|
<

>
|
>
>




<
<







 







<
<




|





<







 







<
<







 








<
<
<
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16
17
18


19
20
21
22
23
24
25
..
33
34
35
36
37
38
39


40
41
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
..
85
86
87
88
89
90
91


92
93
94
95
96
97
98
..
99
100
101
102
103
104
105
106



# encoding: UTF-8
# api: streamtuner2
# type: functions
# title: http download / methods
# description: http utility
# version: 1.4
#
# 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

# Sets either text or percentage of main windows' status bar.
#
................................................................................

  # send to main win
  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; 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.
#
def get(url, params={}, referer="", post=0, ajax=0, binary=0, feedback=None, content=True):
................................................................................
        return r
    elif binary:
        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):
        # remove whitespace
        url = url.strip()
................................................................................
        # add scheme
        if (url.find("://") < 0):
            url = "http://" + url
        # add mandatory path
        if (url.find("/", 10) < 0):
            url = url + "/"
    return url




Modified channels/__init__.py from [2254b1cf48] to [6af40f336b].

1
2
3
4
5
6
7
8
...
500
501
502
503
504
505
506

507


508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
#
# encoding: UTF-8
# api: streamtuner2
# type: base
# category: ui
# title: Channel plugins
# description: Base implementation for channels and feature plugins
# version: 1.1
................................................................................
# channel plugin without glade-pre-defined notebook tab
#
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() """





<







 







>
|
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

<
<
|
|
|
|
|
|
|
|
|
|


|
|

<
|
|
|
<
<
|

<
|
|
|


<
<
<
<

1
2
3
4
5
6
7
...
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558


559
560
561
562
563
564
565
566
567
568
569
570
571
572
573

574
575
576


577
578

579
580
581
582
583





# encoding: UTF-8
# api: streamtuner2
# type: base
# category: ui
# title: Channel plugins
# description: Base implementation for channels and feature plugins
# version: 1.1
................................................................................
# channel plugin without glade-pre-defined notebook tab
#
class ChannelPlugin(GenericChannel):

    module = "abstract"

    def gui(self, parent):

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





Modified channels/bookmarks.py from [de1d64d722] to [1582092482].

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
..
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# Favourite lists.
#
# 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 *



................................................................................
# Sub-plugins simply append a new category, and populate the streams
# list themselves.
#
# 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()











|
<







 







|
|
|
|
|

|
|
|
|
|
|

|
|
|
|
|
|
|
|


|
|
|
|
|
|
|
|
|
|
|

|
|
|
|


|
|
|
|
|
|
|
|


|
|
|


|
|
|
|
|
|


|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|


|
|
|
|
|
|


|
|
|
|
|
|
|
|
|


|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|

|
|
|
|
|
|
|
|


|
|
|
|
|
|
|
|

|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

<
<
<
11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
..
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209



# Favourite lists.
#
# 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 *



................................................................................
# Sub-plugins simply append a new category, and populate the streams
# list themselves.
#
# 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()




Modified channels/file.py from [1ae0a2679f] to [1ed6e9a8dc].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
119
120
121
122
123
124
125


126
127
128
129
130
131
132
#
# api: streamtuner2
# title: File browser
# description: Displays mp3/oggs or m3u/pls files from local media file directories.
# type: channel
# category: local
# version: 0.1
# priority: optional
# 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.
#



# modules
import os
import re

................................................................................

    # read dirs
    def scan_dirs(self):
        self.categories = []
    
        # add main directory
        for main in self.dir:


          if os.path.exists(main):
            self.categories.append(main)
            
            # prepare subdirectories list
            sub = []
            self.categories.append(sub)

<












|
<







 







>
>








1
2
3
4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
...
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

# api: streamtuner2
# title: File browser
# description: Displays mp3/oggs or m3u/pls files from local media file directories.
# type: channel
# category: local
# version: 0.1
# priority: optional
# 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. Presents files from configured directories.




# modules
import os
import re

................................................................................

    # read dirs
    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 = []
            self.categories.append(sub)

Modified channels/history.py from [8bb9924983] to [7a53bd0585].

5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# version: 1.0
# type: group
# 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.
#



from config import *
from channels import *



class history:

    # plugin info
    module = "history"
    title = "History"







<
|
|
<
<




<







5
6
7
8
9
10
11

12
13


14
15
16
17

18
19
20
21
22
23
24
# version: 1.0
# type: group
# 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 recently played streams in a new [history] tab in the
# bookmarks channel.




from config import *
from channels import *



class history:

    # plugin info
    module = "history"
    title = "History"

Modified channels/icast.py from [0fb036d9f5] to [9166297ac1].

15
16
17
18
19
20
21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#   NHrTK3TMRYTCIlmdM3bNKmy8KGGWgrs3UYRXSHp0TISKS4WaPXq4OXrENHrNO3zRPnitXpZ/aqJrRnd5Y5VPWYVXNGeTNnO1M2OiLmy+PYPdSo3eUZHaU5DLg7cxtOwNntIaYpVrMGittLvEKV+lQofd
#   UZXoXp7rY6HfVoutsOUMQnOHdIurM2q2Y6TvbarqZJvFYpBlp9sSs+gMboag3t/dfZCqPWysSoLNYJvgYZSxfq5NNmF6MlmCcoab19nXqKyyZoSwTnawN2OfRGqXXXqanKKjzMzMt7e3o6SjsLKvtVfy
#   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 *
import ahttp as http








>









|
<
<







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32


33
34
35
36
37
38
39
#   NHrTK3TMRYTCIlmdM3bNKmy8KGGWgrs3UYRXSHp0TISKS4WaPXq4OXrENHrNO3zRPnitXpZ/aqJrRnd5Y5VPWYVXNGeTNnO1M2OiLmy+PYPdSo3eUZHaU5DLg7cxtOwNntIaYpVrMGittLvEKV+lQofd
#   UZXoXp7rY6HfVoutsOUMQnOHdIurM2q2Y6TvbarqZJvFYpBlp9sSs+gMboag3t/dfZCqPWysSoLNYJvgYZSxfq5NNmF6MlmCcoab19nXqKyyZoSwTnawN2OfRGqXXXqanKKjzMzMt7e3o6SjsLKvtVfy
#   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 *
import ahttp as http

Modified channels/internet_radio.py from [e1356b6b1d] to [703c455b25].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#
# api: streamtuner2
# title: Internet-Radio
# description: Broad list of webradios from all genres.
# type: channel
# category: radio
# version: 1.2
# 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.
# 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
import ahttp as http
from pq import pq
<











|







<
<
<
<
<








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19





20
21
22
23
24
25
26

# api: streamtuner2
# title: Internet-Radio
# description: Broad list of webradios from all genres.
# type: channel
# category: radio
# version: 1.2
# 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 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
import ahttp as http
from pq import pq

Modified channels/itunes.py from [c564d09eeb] to [48eaa07b1c].

6
7
8
9
10
11
12

13

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# type: channel
# category: radio
# url: http://www.itunes.com?
# priority: optional
# config: -
# 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








>
|
>










<







6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
# type: channel
# category: radio
# url: http://www.itunes.com?
# priority: optional
# config: -
# documentation: http://lab.rolisoft.net/playlists.html
#
#
# API 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

Modified channels/jamendo.py from [3aba6ea22f] to [809614f8f4].

19
20
21
22
23
24
25










26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# doesn't seem overly sensible.
#
# 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.










#
#
# The v3.0 streaming URLs don't seem to work. Therefore some /get2 URLs will
# be used.
#
#  [x]  http://api.jamendo.com/v3.0/playlists/file?client_id=&id=
#  [+]  http://storage-new.newjamendo.com/?trackid=792843&format=ogg2&u=0
#  [+]  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"
    homepage = "http://www.jamendo.com/"
    version = 0.3







>
>
>
>
>
>
>
>
>
>












<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47















48
49
50
51
52
53
54
# doesn't seem overly sensible.
#
# 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.
#
#  [x]  http://api.jamendo.com/v3.0/playlists/file?client_id=&id=
#  [+]  http://storage-new.newjamendo.com/?trackid=792843&format=ogg2&u=0
#  [+]  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.
#















class jamendo (ChannelPlugin):

    # description
    title = "Jamendo"
    module = "jamendo"
    homepage = "http://www.jamendo.com/"
    version = 0.3

Modified channels/live365.py from [f9cb99d01d] to [f028ea638f].

5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
..
37
38
39
40
41
42
43









44
45
46
47
48
49
50
# version: 0.3
# type: channel
# 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
#


# streamtuner2 modules
from config import conf
from uikit import uikit
import ahttp as http
from channels import *
................................................................................
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/"







|
|
|
<
<
<
<
<
<







 







>
>
>
>
>
>
>
>
>







5
6
7
8
9
10
11
12
13
14






15
16
17
18
19
20
21
..
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# version: 0.3
# type: channel
# category: radio
# url: http://www.live365.com/
# config: -
# priority: optional
# 
# 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
import ahttp as http
from channels import *
................................................................................
import urllib
from itertools import groupby
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"
    homepage = "http://www.live365.com/"
    base_url = "http://www.live365.com/"

Modified channels/modarchive.py from [d62c0ac39b] to [4800565f88].

5
6
7
8
9
10
11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29
30
31

32



33
34
35
36
37
38
39
40
41
# type: channel
# version: 0.2
# url: http://www.modarchive.org/
# priority: extra
# config: -
# category: collection
#
#
# 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.
#

# Modarchive actually provides an API
# http://modarchive.org/index.php?xml-api
# (If only it wasn't XML based..)
#


import re
import ahttp as http
from config import conf
from channels import *
from config import __print__, dbg








# MODs
class modarchive (ChannelPlugin):

    # description
    title = "modarchive"
    module = "modarchive"
    homepage = "http://www.modarchive.org/"
    base = "http://modarchive.org/"







|
<



<

>
|
<
<
<









>
|
>
>
>
|
<







5
6
7
8
9
10
11
12

13
14
15

16
17
18



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

34
35
36
37
38
39
40
# type: channel
# version: 0.2
# url: http://www.modarchive.org/
# priority: extra
# config: -
# category: collection
#
# 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.

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


# 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"
    homepage = "http://www.modarchive.org/"
    base = "http://modarchive.org/"

Modified channels/myoggradio.py from [ac171b964b] to [23fa6f6672].

1
2
3
4
5
6
7
8
..
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
177
178
179
180
181
182
183
184
185
186
187
#
# api: streamtuner2
# title: MyOggRadio
# description: Open source internet radio directory.
# type: channel
# category: radio
# version: 0.6
# url: http://www.myoggradio.org/
................................................................................
# 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
from uikit import uikit
import ahttp as http
................................................................................
        if len(conf.myoggradio_login) and conf.myoggradio_login != "user:password":
            return conf.myoggradio_login.split(":")
        else:
            lap =  conf.netrc(["myoggradio", "myoggradio.org", "www.myoggradio.org"])
            if lap:
                return [lap[0] or lap[1], lap[2]]
        pass        




|







 







<







 







<



1
2
3
4
5
6
7
8
..
16
17
18
19
20
21
22

23
24
25
26
27
28
29
...
176
177
178
179
180
181
182

183
184
185

# api: streamtuner2
# title: MyOggRadio
# description: Open source internet radio directory.
# type: channel
# category: radio
# version: 0.6
# url: http://www.myoggradio.org/
................................................................................
# 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
from uikit import uikit
import ahttp as http
................................................................................
        if len(conf.myoggradio_login) and conf.myoggradio_login != "user:password":
            return conf.myoggradio_login.split(":")
        else:
            lap =  conf.netrc(["myoggradio", "myoggradio.org", "www.myoggradio.org"])
            if lap:
                return [lap[0] or lap[1], lap[2]]
        pass        




Modified channels/search.py from [5610a2549a] to [2947558d4e].

7
8
9
10
11
12
13



14
15
16
17
18
19
20
# config: -
# priority: core
# 
# Configuration dialog for audio applications,
# general settings, and plugin activation and
# associated options.
#





from uikit import *
import channels
from config import *
from copy import copy








>
>
>







7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# config: -
# priority: core
# 
# 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 *
from copy import copy

Modified channels/shoutcast.py from [d4f5d3a425] to [41fdc8e85d].

10
11
12
13
14
15
16

17
18
19
20
21














22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# url: http://www.shoutcast.com/
# config: -
# 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 
#














# 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
#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"
    base_url = "http://shoutcast.com/"
    listformat = "audio/x-scpls"







>

|
|
<
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>











<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46















47
48
49
50
51
52
53
# url: http://www.shoutcast.com/
# config: -
# 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 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:
#
#   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.
#















class shoutcast(channels.ChannelPlugin):

    # desc
    module = "shoutcast"
    title = "SHOUTcast"
    base_url = "http://shoutcast.com/"
    listformat = "audio/x-scpls"

Modified channels/tunein.py from [35eb51b3bf] to [8f4dc51ffa].

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#
# RadioTime API uses OPML for station/podcast entries.
#
# 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__
from channels import *
import ahttp as http







<







13
14
15
16
17
18
19

20
21
22
23
24
25
26
#
# RadioTime API uses OPML for station/podcast entries.
#
# 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__
from channels import *
import ahttp as http

Modified channels/xiph.py from [5e3d628269] to [08e11d5433].

6
7
8
9
10
11
12
13

14
15





















16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# url: http://dir.xiph.org/
# version: 0.3
# 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.
#





















# It 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.
#
# 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",
          ]








|
>
|

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|




|
|






|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511






















# url: http://dir.xiph.org/
# version: 0.3
# 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.
#
# 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
#
#
# 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.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
#
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",
    ]























Modified channels/youtube.py from [cc1c8191ad] to [d1ebc01baa].

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# a few of the details.
# While .wrap3() unpacks the various variations of where the video IDs
# 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")
#  api("channels", categoryId=10)
#  api("search", topicId="/m/064t9", type="video")







<







41
42
43
44
45
46
47

48
49
50
51
52
53
54
# a few of the details.
# While .wrap3() unpacks the various variations of where the video IDs
# 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")
#  api("channels", categoryId=10)
#  api("search", topicId="/m/064t9", type="video")