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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [8b3cd06ff7]

Overview
Comment:Adapted Live365 channel plugin for /cgi-bin/play.pls?stationid=123457&direct=1 stream URLs instead of extraction, works again
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 8b3cd06ff708ba014f3e677c8f2973daae51454f
User & Date: mario on 2014-07-31 17:22:46
Other Links: manifest | tags
Context
2014-08-01
01:34
catmap{} cache handling now by _generic module check-in: cda3504633 user: mario tags: trunk
2014-07-31
17:22
Adapted Live365 channel plugin for /cgi-bin/play.pls?stationid=123457&direct=1 stream URLs instead of extraction, works again check-in: 8b3cd06ff7 user: mario tags: trunk
03:23
Fixed invalid encoding in (manually edited) releases.json check-in: e5effdd595 user: mario tags: trunk
Changes

Modified _package.epm from [81c1d8ed48] to [84031fd279].

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







%product streamtuner2 - internet radio browser
%version 2.1.1
%version 2.1.2
%vendor Mario Salzer
%license
%copyright Placed into the Public Domain, 2009-2014
%readme README

%description Browser for Internet Radio Stations
%description   .
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
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







-
+













+
+


+
+







f 755 root root /usr/bin/streamtuner2				./st2.py
f 644 root root /usr/share/applications/streamtuner2.desktop	./streamtuner2.desktop
d 755 root root /usr/share/streamtuner2				-
f 644 root root /usr/share/streamtuner2/streamtuner2.png	./streamtuner2.png
f 644 root root /usr/share/pixmaps/streamtuner2.png		./logo.png
f 644 root root /usr/share/streamtuner2/gtk2.xml		./gtk2.xml
f 644 root root /usr/share/streamtuner2/gtk3.xml		./gtk3.xml
#f 644 root root /usr/share/streamtuner2/pson.py			./pson.py
#f 644 root root /usr/share/streamtuner2/pson.py		./pson.py
#f 644 root root /usr/share/streamtuner2/processing.py		./processing.py
f 644 root root /usr/share/streamtuner2/compat2and3.py		./compat2and3.py
f 644 root root /usr/share/streamtuner2/action.py		./action.py
f 644 root root /usr/share/streamtuner2/config.py		./config.py
f 644 root root /usr/share/streamtuner2/ahttp.py		./ahttp.py
f 644 root root /usr/share/streamtuner2/cli.py			./cli.py
f 644 root root /usr/share/streamtuner2/mygtk.py		./mygtk.py
f 644 root root /usr/share/streamtuner2/favicon.py		./favicon.py
f 644 root root /usr/share/streamtuner2/pq.py			./pq.py
#-- channels
d 755 root root /usr/share/streamtuner2/channels		-
f 644 root root /usr/share/streamtuner2/channels/__init__.py	./channels/__init__.py
f 644 root root /usr/share/streamtuner2/channels/_generic.py	./channels/_generic.py
f 644 root root /usr/share/streamtuner2/channels/icast.py 	./channels/icast.py
f 644 root root /usr/share/streamtuner2/channels/icast.png 	./channels/icast.png
f 644 root root /usr/share/streamtuner2/channels/internet_radio.py ./channels/internet_radio.py
f 644 root root /usr/share/streamtuner2/channels/internet_radio.png ./channels/internet_radio.png
f 644 root root /usr/share/streamtuner2/channels/itunes.py 	./channels/itunes.py
f 644 root root /usr/share/streamtuner2/channels/itunes.png 	./channels/itunes.png
f 644 root root /usr/share/streamtuner2/channels/jamendo.py 	./channels/jamendo.py
f 644 root root /usr/share/streamtuner2/channels/jamendo.png 	./channels/jamendo.png
f 644 root root /usr/share/streamtuner2/channels/live365.py	./channels/live365.py
f 644 root root /usr/share/streamtuner2/channels/live365.png 	./channels/live365.png
f 644 root root /usr/share/streamtuner2/channels/modarchive.py 	./channels/modarchive.py
f 644 root root /usr/share/streamtuner2/channels/modarchive.png	./channels/modarchive.png
f 644 root root /usr/share/streamtuner2/channels/musicgoal.py 	./channels/musicgoal.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
75
76
77
78
79
80
81


82
83
84
85
86
87
88







-
-







f 644 root root /usr/share/streamtuner2/channels/youtube.png	./channels/youtube.png
f 644 root root /usr/share/streamtuner2/channels/global_key.py 	./channels/global_key.py
f 644 root root /usr/share/streamtuner2/channels/links.py 	./channels/links.py
f 644 root root /usr/share/streamtuner2/channels/timer.py 	./channels/timer.py
#-- scripts
#d 755 root root /usr/share/streamtuner2/scripts		-
#f 644 root root /usr/share/streamtuner2/scripts/radiotop40_de.py  ./scripts/radiotop40_de.py
#-- themes
#f 644 root root /usr/share/streamtuner2/themes/MountainDew/gtk-2.0/gtkrc ./themes/MountainDew/gtk-2.0/gtkrc
#-- help files
f 644 root root /usr/share/man/man1/streamtuner2.1		 	 ./help/streamtuner2.1
d 755 root root /usr/share/doc/streamtuner2/help 			-
f 644 root root /usr/share/doc/streamtuner2/help/action_homepage.page 	 ./help/action_homepage.page
f 644 root root /usr/share/doc/streamtuner2/help/action_playing.page 	 ./help/action_playing.page
f 644 root root /usr/share/doc/streamtuner2/help/action_recording.page 	 ./help/action_recording.page
f 644 root root /usr/share/doc/streamtuner2/help/action_saving.page 	 ./help/action_saving.page

Modified ahttp.py from [31cf3bd93f] to [0fe1c60aea].

52
53
54
55
56
57
58
59

60
61
62
63
64
65
66
52
53
54
55
56
57
58

59
60
61
62
63
64
65
66







-
+










#-- 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):
def get(url, params={}, referer="", post=0, ajax=0, binary=0, feedback=None, content=True):
    __print__( dbg.HTTP, "GET", url, params )

    # statusbar info
    progress_feedback(url)
    
    # combine headers
    headers = {}
74
75
76
77
78
79
80
81

82
83
84


85
86
87
88








89
90
91
92
93
94
95
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







-
+

-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+







        r = session.post(url, params=params, headers=headers)
    else:    
        r = session.get(url, params=params, headers=headers)

    __print__( dbg.HTTP, r.request.headers );
    __print__( dbg.HTTP, r.headers );
            
    # result
    # finish, clean statusbar
    #progress_feedback(0.9)
    content = (r.content if binary else r.text)
    
    progress_feedback("")

    # finish, clean statusbar
    progress_feedback("")
    __print__( dbg.INFO, "Content-Length", len(content) )
    return content
    # result
    __print__( dbg.INFO, "Content-Length", len(r.content) )
    if not content:
        return r
    elif binary:
        return r.content
    else:
        return r.text




#-- Append missing trailing slash to URLs
def fix_url(url):
    if url is None:

Modified channels/file.py from [a8e2e93dda] to [f92686b376].

1
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16
1
2
3
4
5
6
7
8

9
10
11
12
13
14
15
16








-
+







#
# api: streamtuner2
# title: File browser
# description: Displays mp3/oggs or m3u/pls files from local media file directories.
# type: channel
# category: media
# version: 0.0
# priority: optional
# depends: mutagen, kiwi
# depends: mutagen
#
#
# Local file browser.
#
#


176
177
178
179
180
181
182


183
184
185
176
177
178
179
180
181
182
183
184
185
186
187







+
+



    def update_categories(self):
        self.scan_dirs()

        
    # same as init
    def update_streams(self, cat, x=0):
        self.scan_dirs()
        print(self.streams)
        print(self.categories)
        return self.streams.get(os.path.basename(cat))


Modified channels/live365.py from [861e1f12f9] to [be1047f842].

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
33
34
35
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49









-
-
-
+
-
-
+
+
+
+

-
+
-
-

+

-
+
-
-
+




















+








# api: streamtunter2
# title: Live365
# description: Around 5000 categorized internet radio streams, some paid ad-free ones.
# type: channel
# category: radio
# version: 0.3
# priority: optional
#
# 2.1.2 broken,
# new URLs:
#
# 
#   GET /cgi-bin/mini.cgi?version=3&templateid=xml&from=web&site=web&caller=&tag=web&station_name=bofbm&_=1404610275892
#      <NANOCASTER_PARAMS> (session id)
#
# We're currently extracting from the JavaScript;
#
#    stn.set("param", "value");
#
#   GET /play?now=59&membername=&session=1404610276-475426&tag=web&s=bofbm&d=LIVE365&r=0
# And using a HTML5 player direct URL now:
#       &app_id=web%3ABROWSER&token=b99d7f579bacab06b9baa1502d53bedc-3101060080001248&AuthType=NORMAL
#       &lid=276006-deu&SaneID=178.24.130.71-1404610229579
#
#    /cgi-bin/play.pls?stationid=%s&direct=1&file=%s.pls
#

#
raise Exception

#


# streamtuner2 modules
from config import conf
from mygtk import mygtk
import ahttp as http
from channels import *
from config import __print__, dbg
import action

# python modules
import re
import xml.dom.minidom
from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities
import gtk
import copy
import urllib
from itertools import groupby
from time import time
from xml.dom.minidom import parseString


# channel live365
class live365(ChannelPlugin):

    # desc
    module = "live365"
    title = "Live365"
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





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







-
-
-
+
+
+
+
+
+








-
-
+
+



-
+





-
+




-
+

-

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-








-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
-
+
+
+
+
+
    # fixed for now
    def update_categories(self):
        pass


    # extract stream infos
    def update_streams(self, cat):
    
        url = "http://www.live365.com/genres/%s" % cat.lower()
        html = http.get(url, feedback=self.parent.status)

        # Retrieve genere index pages    
        html = ""
        for i in [1, 17, 33, 49]:
            url = "http://www.live365.com/cgi-bin/directory.cgi?first=%i&site=web&mode=3&genre=%s&charset=UTF-8&target=content" % (i, cat.lower())
            html += http.get(url, feedback=self.parent.status)
        
        # Extract from JavaScript       
        rx = re.compile(r"""
                stn.set\(   " (\w+) ", \s+  " ((?:[^"\\]+|\\.)*) "\);  \s+
            """, re.X|re.I|re.S|re.M)

        # Group entries before adding them
        ls = []
        for i,g in groupby(rx.findall(html), self.group_by_station):
            row = dict(g)
        for i,row in groupby(rx.findall(html), self.group_by_station):
            row = dict(row)
            ls.append({
                "name": row["stationName"],
                "title": row["title"],
                "playing": "",
                "playing": "n/a",
                "id": row["id"],
                "access": row["listenerAccess"],
                "status": row["status"],
                "mode": row["serverMode"],
                "rating": int(row["rating"]),
                "rating": row["ratingCount"],
                #"rating": row["ratingCount"],
                "listeners": int(row["tlh"]),
                "location": row["location"],
                "favicon": row["imgUrl"],
                "format": self.mediatype,
                "url": "urn:live365:%s:%s" % (row["id"], row["stationName"])
                "url": "%scgi-bin/play.pls?stationid=%s&direct=1&file=%s.pls" % (self.base_url, row["id"], row["stationName"])
            })
        print ls
        return ls

    # inject session id etc. into direct audio url
    def play(self, row):
        if row.get("url"):

            # params
            id = row["id"]
            name = row["name"]

            # get session
            mini = "http://www.live365.com/cgi-bin/mini.cgi?version=3&templateid=xml&from=web&site=web" \
                 + "&caller=&tag=web&station_name=%s&_=%i111" % (name, time())
            xml = parseString(http.get(mini)).getElementsByTagName("LIVE365_PLAYER_WINDOW")[0]
            x = lambda name: xml.getElementsByTagName(name)[0].childNodes[0].data

            # mk audio url
            play =  "http://www.live365.com/play?now=0&" \
                 + x("NANOCASTER_PARAMS") \
                 + "&token=" + x("TOKEN") \
                 + "&AuthType=NORMAL&lid=276006-deu&SaneID=178.24.130.71-1406763621701"
            __print__(dbg.DATA, play)
            
            # let's see what happens
            action.action.play(play, self.mediatype, self.listformat)

            


    # itertools.groupby filter
    gi = 0
    def group_by_station(self, kv):
        if kv[0] == "stationName":
            self.gi += 1
        return self.gi

    # we can no longer cache all the things
    def cache(self):
        pass
    def save(self):
        pass

    # inject session id etc. into direct audio url
    def UNUSED_play(self, row):
        if row.get("url"):

            # params
            id = row["id"]
            name = row["name"]

            # get mini.cgi station resource
            mini_url = "http://www.live365.com/cgi-bin/mini.cgi?version=3&templateid=xml&from=web&site=web" \
                 + "&caller=&tag=web&station_name=%s&_=%i111" % (name, time())
            mini_r = http.get(mini_url, content=False)
            mini_xml = parseString(mini_r.text).getElementsByTagName("LIVE365_PLAYER_WINDOW")[0]
            mini = lambda name: mini_xml.getElementsByTagName(name)[0].childNodes[0].data
            
            # authorize with play.cgi
            play_url = ""

            # mk audio url
            play =  "http://%s/play" % mini("STREAM_URL") \
                 + "?now=0&" \
                 + mini("NANOCASTER_PARAMS") \
                 + "&token=" + mini("TOKEN") \
                 + "&AuthType=NORMAL&lid=276006-deu&SaneID=178.24.130.71-1406763621701"

            
            # let's see what happens
            action.action.play(play, self.mediatype, self.listformat)


Modified st2.py from [a922a58f51] to [3d89c6ea6f].

1
2
3
4
5
6
7
8

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

8
9
10
11
12
13
14
15







-
+







#!/usr/bin/env python
# encoding: UTF-8
# api: python
# type: application
# title: streamtuner2
# description: Directory browser for internet radio / audio streams
# depends: pygtk | pygi, threading, pyquery, kronos, requests
# version: 2.1.1
# version: 2.1.2
# author: mario salzer
# license: public domain
# url: http://freshmeat.net/projects/streamtuner2
# config: <env name="http_proxy" value="" description="proxy for HTTP access" />  <env name="XDG_CONFIG_HOME" description="relocates user .config subdirectory" />
# category: multimedia
# 
#
92
93
94
95
96
97
98
99

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

99
100
101
102
103
104
105
106







-
+







import ahttp
import action  # needs workaround... (action.main=main)
import channels
from channels import *
import favicon


__version__ = "2.1.1"
__version__ = "2.1.2"


# this represents the main window
# and also contains most application behaviour
main = None
class StreamTunerTwo(gtk.Builder):