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

⌈⌋ branch:  streamtuner2


Check-in [220ee1286a]

Overview
Comment:Exchange audio/mp3 for standard audio/mpeg MIME type.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 220ee1286a7b08ab5b75daf426749b9ea34fcba6
User & Date: mario on 2014-05-13 21:00:06
Other Links: manifest | tags
Context
2014-05-13
21:04
Mirror config dialog changes to Gtk3 ui file check-in: 74bf77f074 user: mario tags: trunk
21:00
Exchange audio/mp3 for standard audio/mpeg MIME type. check-in: 220ee1286a user: mario tags: trunk
19:58
Some surfmusik category fixes, support for TV channel retrieval check-in: 3e7da2fdba user: mario tags: trunk
Changes

Modified action.py from [a3c14ebdbb] to [5d3890f993].

37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
37
38
39
40
41
42
43

44
45
46
47
48
49
50
51







-
+







# but also "browser" for web URLs
#        
class action:

        # streamlink formats
        lt = {"asx":"video/x-ms-asf", "pls":"audio/x-scpls", "m3u":"audio/x-mpegurl", "xspf":"application/xspf+xml", "href":"url/http", "ram":"audio/x-pn-realaudio", "smil":"application/smil"}
        # media formats
        mf = {"mp3":"audio/mp3", "ogg":"audio/ogg", "aac":"audio/aac"}
        mf = {"mp3":"audio/mpeg", "ogg":"audio/ogg", "aac":"audio/aac"}
        
        
        # web
        @staticmethod
        def browser(url):
            __print__( dbg.CONF, conf.browser )
            action.run(conf.browser + " " + action.quote(url))
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
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







-
+



-
-
+
+



















-
+







                return str(s)   # should actually be "\\\"%s\\\"" % s
            else:
                return "%r" % str(s)


        # calls player for stream url and format
        @staticmethod
        def play(url, audioformat="audio/mp3", listformat="text/x-href"):
        def play(url, audioformat="audio/mpeg", listformat="text/x-href"):
            if (url):
                url = action.url(url, listformat)
            if (audioformat):
                if audioformat == "audio/mpeg":
                    audioformat = "audio/mp3"  # internally we use the more user-friendly moniker
                if audioformat == "audio/mp3":
                    audioformat = "audio/mpeg"
                cmd = conf.play.get(audioformat, conf.play.get("*/*", "vlc %u"))
                __print__( dbg.PROC,"play", url, cmd )
            try:
                action.run( action.interpol(cmd, url) )
            except:
                pass

        
        # exec wrapper
        @staticmethod
        def run(cmd):
            if conf.windows:
                os.system("start \"%s\"")
            else:
                os.system(cmd + " &")


        # streamripper
        @staticmethod
        def record(url, audioformat="audio/mp3", listformat="text/x-href", append="", row={}):
        def record(url, audioformat="audio/mpeg", listformat="text/x-href", append="", row={}):
            __print__( dbg.PROC, "record", url )
            cmd = conf.record.get(audioformat, conf.record.get("*/*", None))
            try: action.run( action.interpol(cmd, url, row) + append )
            except: pass


        # save as .m3u
207
208
209
210
211
212
213
214

215
216
217
218
219
220
221
207
208
209
210
211
212
213

214
215
216
217
218
219
220
221







-
+







            # extract stream address from .pls URL
            if (re.search("\.pls", pls)):       #audio/x-scpls
                return action.pls(pls)
            elif (re.search("\.asx", pls)):	#video/x-ms-asf
                return re.findall("<Ref\s+href=\"(http://.+?)\"", http.get(pls))
            elif (re.search("\.m3u|\.ram|\.smil", pls)):	#audio/x-mpegurl
                return re.findall("(http://[^\s]+)", http.get(pls), re.I)
            else:  # just assume it was a direct mp3/ogg streamserver link
            else:  # just assume it was a direct mpeg/ogg streamserver link
                return [ (pls if pls.startswith("/") else http.fix_url(pls)) ]
            pass


        # generate filename for temporary .m3u, if possible with unique id
        @staticmethod
        def tmp_fn(pls):

Modified channels/_generic.py from [3af550db05] to [e7ef47afa8].

48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
48
49
50
51
52
53
54

55
56
57
58
59
60
61
62







-
+








        # desc
        module = "generic"
        title = "GenericChannel"
        homepage = "http://milki.inlcude-once.org/streamtuner2/"
        base_url = ""
        listformat = "audio/x-scpls"
        audioformat = "audio/mp3" # fallback value
        audioformat = "audio/mpeg" # fallback value
        config = []
        has_search = False

        # categories
        categories = ["empty", ]
        current = ""
        default = "empty"
427
428
429
430
431
432
433
434

435
436

437
438
439
440
441
442
443
427
428
429
430
431
432
433

434
435

436
437
438
439
440
441
442
443







-
+

-
+







            
        # convert audio format nick/shortnames to mime types, e.g. "OGG" to "audio/ogg"
        def mime_fmt(self, s):
            # clean string
            s = s.lower().strip()
            # rename
            map = {
                "audio/mpeg":"audio/mp3",  # Note the real mime type is /mpeg, but /mp3 is more understandable in the GUI
                "audio/mp3":"audio/mpeg",  # Note the real mime type is /mpeg, but /mp3 is more understandable in the GUI
                "ogg":"ogg", "ogm":"ogg", "xiph":"ogg", "vorbis":"ogg", "vnd.xiph.vorbis":"ogg",
                "mpeg":"mp3", "mp":"mp3", "mp2":"mp3", "mpc":"mp3", "mps":"mp3",
                "mp3":"mpeg", "mp":"mpeg", "mp2":"mpeg", "mpc":"mpeg", "mps":"mpeg",
                "aac+":"aac", "aacp":"aac",
                "realaudio":"x-pn-realaudio", "real":"x-pn-realaudio", "ra":"x-pn-realaudio", "ram":"x-pn-realaudio", "rm":"x-pn-realaudio",
                # yes, we do video
                "flv":"video/flv", "mp4":"video/mp4",
            }
            map.update(action.action.lt)   # list type formats (.m3u .pls and .xspf)
            if map.get(s):

Modified channels/internet_radio_org_uk.py from [2ca4dac6b1] to [f41506948c].

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







-
+


















-
+







                        "url": url,
                        "genre": self.strip_tags(genre),
                        "homepage": http.fix_url(homepage),
                        "title": title,
                        "playing": playing,
                        "bitrate": int(bitrate),
                        "listeners": int(listeners if listeners else 0),
                        "format": "audio/mp3", # there is no stream info on that, but internet-radio.org.uk doesn't seem very ogg-friendly anyway, so we assume the default here
                        "format": "audio/mpeg", # there is no stream info on that, but internet-radio.org.uk doesn't seem very ogg-friendly anyway, so we assume the default here
                    })

            # DOM parsing
            else:
                # the streams are arranged in table rows
                doc = pq(html)
                for dir in (pq(e) for e in doc("tr.stream")):
                    
                    bl = dir.find("td[align=right]").text()
                    bl = rx_numbers.findall(str(bl) + " 0 0")
                    
                    entries.append({
                        "title": dir.find("b").text(),
                        "homepage": http.fix_url(dir.find("a.url").attr("href")),
                        "url": dir.find("a").eq(2).attr("href"),
                        "genre": dir.find("td").eq(0).text(),
                        "bitrate": int(bl[0]),
                        "listeners": int(bl[1]),
                        "format": "audio/mp3",
                        "format": "audio/mpeg",
                        "playing": dir.find("td").eq(1).children().remove().end().text()[13:].strip(),
                    })
            
            # next page?
            if str(page+1) not in rx_pages.findall(html):
                max = 0
            else:

Modified channels/punkcast.py from [e751e37b9b] to [0e12904925].

70
71
72
73
74
75
76
77

78
79
80
81
82
83
84
70
71
72
73
74
75
76

77
78
79
80
81
82
83
84







-
+







        #-- all from frontpage
        for uu in rx_link.findall(http.get(self.homepage)):
            (homepage, id, title) = uu
            entries.append({
                    "genre": "?",
                    "title": title,
                    "playing": "PUNKCAST #"+id,
                    "format": "audio/mp3",
                    "format": "audio/mpeg",
                    "homepage": homepage,
            })

        # done    
        return entries


Modified channels/timer.py from [57941064a8] to [5079381818].

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







-
+

















-
+








        except:
            return 0   # no limit
        
    # action wrapper
    def play(self, row, *args, **kwargs):
        action.play(
            url = row["url"],
            audioformat = row.get("format","audio/mp3"), 
            audioformat = row.get("format","audio/mpeg"), 
            listformat = row.get("listformat","url/direct"),
        )

    # action wrapper
    def record(self, row, *args, **kwargs):
        #print("TIMED RECORD")
        
        # extra params
        duration = self.duration(row.get(self.timefield))
        if duration:
            append = " -a %S.%d.%q -l "+str(duration*60)   # make streamripper record a whole broadcast
        else:
            append = ""

        # start recording
        action.record(
            url = row["url"],
            audioformat = row.get("format","audio/mp3"), 
            audioformat = row.get("format","audio/mpeg"), 
            listformat = row.get("listformat","url/direct"),
            append = append,
        )
    
    def test(self, row, *args, **kwargs):
        print("TEST KRONOS", row)


Modified cli.py from [ea53579a96] to [b62ad1b334].

105
106
107
108
109
110
111
112

113
114
115
116
117
118
119
105
106
107
108
109
110
111

112
113
114
115
116
117
118
119







-
+







        if row.get("url"):
            print(row["url"])
            
    # run player
    def play(self, *args):
        row = self.stream(*args)
        if row.get("url"):
            #action.action.play(row["url"], audioformat=row.get("format","audio/mp3"))
            #action.action.play(row["url"], audioformat=row.get("format","audio/mpeg"))
            self.plugins[self.current_channel].play(row)
            
    # return cache data 1:1
    def dump(self, channel):
        c = self.channel(channel)
        c.cache()
        return c.streams

Modified config.py from [9f0935794a] to [5e2372f9f1].

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







+










-
+







            dirs = ["/usr/share/streamtuner2", "/usr/local/share/streamtuner2", sys.path[0], "."]
            self.share = [d for d in dirs if os.path.exists(d)][0]
            
            # settings from last session
            last = self.load("settings")
            if (last):
                self.update(last)
                self.migrate()
            # store defaults in file
            else:
                self.save("settings")
                self.firstrun = 1


        # some defaults
        def defaults(self):
            self.browser = "sensible-browser"
            self.play = {
               "audio/mp3": "audacious ",	# %u for url to .pls, %g for downloaded .m3u
               "audio/mpeg": "audacious ",	# %u for url to .pls, %g for downloaded .m3u
               "audio/ogg": "audacious ",
               "audio/aac": "amarok -l ",
               "audio/x-pn-realaudio": "vlc --one-instance",
               "audio/*": "totem ",
               "*/*": "vlc --one-instance %srv",
            }
            self.record = {
176
177
178
179
180
181
182







183
184
185
186
187
188
189
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197







+
+
+
+
+
+
+







            for key,value in with_new_data.items():
                if type(value) == dict:
                    self[key].update(value)
                else:
                    self[key] = value
            # descends into sub-dicts instead of wiping them with subkeys

        # update old setting names
        def migrate(self):
            # 2.1.1
            if "audio/mp3" in self.play:
                self.play["audio/mpeg"] = self.play["audio/mp3"]
                del self.play["audio/mp3"]

             
        # check for existing filename in directory list
        def find_in_dirs(self, dirs, file):
            for d in dirs:
                if os.path.exists(d+"/"+file):
                    return d+"/"+file

Modified gtk2.xml from [17490ef6c9] to [99e1222436].

1214
1215
1216
1217
1218
1219
1220
1221

1222
1223
1224
1225
1226
1227
1228
1214
1215
1216
1217
1218
1219
1220

1221
1222
1223
1224
1225
1226
1227
1228







-
+







                                <property name="left_attach">1</property>
                                <property name="right_attach">2</property>
                                <property name="top_attach">12</property>
                                <property name="bottom_attach">13</property>
                              </packing>
                            </child>
                            <child>
                              <object class="GtkEntry" id="config_play_audio_mp3">
                              <object class="GtkEntry" id="config_play_audio_mpeg">
                                <property name="width_request">200</property>
                                <property name="height_request">20</property>
                                <property name="visible">True</property>
                                <property name="can_focus">True</property>
                                <property name="invisible_char">●</property>
                                <property name="invisible_char_set">True</property>
                                <property name="primary_icon_activatable">False</property>
1237
1238
1239
1240
1241
1242
1243
1244


1245
1246
1247
1248
1249
1250
1251
1237
1238
1239
1240
1241
1242
1243

1244
1245
1246
1247
1248
1249
1250
1251
1252







-
+
+







                                <property name="bottom_attach">2</property>
                              </packing>
                            </child>
                            <child>
                              <object class="GtkLabel" id="label7">
                                <property name="visible">True</property>
                                <property name="can_focus">False</property>
                                <property name="label" translatable="yes">audio/mp3</property>
                                <property name="label"
                                translatable="yes">audio/mpeg</property>
                              </object>
                              <packing>
                                <property name="top_attach">1</property>
                                <property name="bottom_attach">2</property>
                              </packing>
                            </child>
                            <child>

Modified st2.py from [6bfb3a77f9] to [22d6d5d44f].

310
311
312
313
314
315
316
317

318
319
320
321
322
323
324
310
311
312
313
314
315
316

317
318
319
320
321
322
323
324







-
+







                self.channel().play(row)
                favicon.download_playing(row)


        # streamripper
        def on_record_clicked(self, widget):
            row = self.row()
            action.record(row.get("url"), "audio/mp3", "url/direct", row=row)
            action.record(row.get("url"), "audio/mpeg", "url/direct", row=row)


        # browse stream
        def on_homepage_stream_clicked(self, widget):
            url = self.selected("homepage")             
            action.browser(url)

721
722
723
724
725
726
727
728

729
730
731
732
733
734
735
721
722
723
724
725
726
727

728
729
730
731
732
733
734
735







-
+







            main.channel().save()
            self.cancel(w)

            
        # add a new list entry, update window
        def new(self, w):
            s = main.channel().stations()
            s.append({"title":"new", "url":"", "format":"audio/mp3", "genre":"", "listeners":1});
            s.append({"title":"new", "url":"", "format":"audio/mpeg", "genre":"", "listeners":1});
            main.channel().switch() # update display
            main.channel().gtk_list.get_selection().select_path(str(len(s)-1)); # set cursor to last row
            self.open(w)


        # hide window
        def cancel(self, *w):
766
767
768
769
770
771
772
773

774
775
776
777
778
779
780
766
767
768
769
770
771
772

773
774
775
776
777
778
779
780







-
+







        # set/load values between gtk window and conf. dict
        def apply(self, config, prefix="config_", save=0):
            for key,val in config.items():
                # map non-alphanumeric chars from config{} to underscores in according gtk widget names
                id = re.sub("[^\w]", "_", key)
                w = main.get_widget(prefix + id)
                __print__(dbg.CONF, "config", ("save" if save else "load"), prefix+id, w, val)
                # recurse into dictionaries, transform: conf.play.audio/mp3 => conf.play_audio_mp3
                # recurse into dictionaries, transform: conf.play.audio/mpeg => conf.play_audio_mpeg
                if (type(val) == dict):
                    self.apply(val, prefix + id + "_", save)
                # load or set gtk.Entry text field
                elif (w and save and type(w)==gtk.Entry):
                    config[key] = w.get_text()
                elif (w and type(w)==gtk.Entry):
                    w.set_text(str(val))