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

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


Check-in [23bbd97989]

Overview
Comment:Introduce action.handler{} callbacks to convert custom streaming URL types such as "audio/soundcloud". Unify backend code for .play/record/browser() calls. Reddit module just splits out domain name now, then checks for walledgarden links (filter option renamed). Introduce url_soundcloud plugin in favour of `soundcli` cmdline client setting.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 23bbd979893d140c1d5cdbd7e3f0ad465084391e
User & Date: mario on 2015-05-24 14:19:06
Other Links: manifest | tags
Context
2015-05-24
16:57
Fix audioformat to audio/mpeg. check-in: fa5df72f08 user: mario tags: trunk
14:19
Introduce action.handler{} callbacks to convert custom streaming URL types such as "audio/soundcloud". Unify backend code for .play/record/browser() calls. Reddit module just splits out domain name now, then checks for walledgarden links (filter option renamed). Introduce url_soundcloud plugin in favour of `soundcli` cmdline client setting. check-in: 23bbd97989 user: mario tags: trunk
10:02
Move channel.save() after column updating. check-in: 3072c80d83 user: mario tags: trunk
Changes

Modified action.py from [005725eacf] to [0cb40c6df9].

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
]

# Preferred probing order of known formats
playlist_fmt_prio = [
   "pls", "xspf", "asx", "smil", "jamj", "json", "m3u", "asf", "raw"
]








# Exec wrapper
#
def run(cmd):
    log.EXEC(cmd)
    try:    os.system("start \"%s\"" % cmd if conf.windows else cmd + " &")
    except: log.ERR("Command not found:", cmd)


# Start web browser
#
def browser(url):
    bin = conf.play.get("url/http", "sensible-browser")
    log.EXEC(bin)
    run(bin + " " + quote(url))


# Open help browser, streamtuner2 pages
#
def help(*args):
    run("yelp /usr/share/doc/streamtuner2/help/")


# Calls player for stream url and format
#
def play(row={}, audioformat="audio/mpeg", source="pls", url=None):





    cmd = mime_app(audioformat, conf.play)
    cmd = interpol(cmd, url or row["url"], source, row)
    run(cmd)





# Call streamripper


#

def record(row={}, audioformat="audio/mpeg", source="href", url=None):
    cmd = mime_app(audioformat, conf.record)
    cmd = interpol(cmd, url or row["url"], source, row)
    run(cmd)


# OS shell command escaping
#
def quote(ins):
    if type(ins) is list:
        return " ".join(["%r" % str(s) for s in ins])







>
>
>
>
>



<





<
<
<
<
<
<
<
<
<

<



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

>
>
>

|
>
>
|
>

|
<
<







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
]

# Preferred probing order of known formats
playlist_fmt_prio = [
   "pls", "xspf", "asx", "smil", "jamj", "json", "m3u", "asf", "raw"
]

# custom stream domain handlers
handler = {
    # "soundcloud": callback(),
}



# Exec wrapper

def run(cmd):
    log.EXEC(cmd)
    try:    os.system("start \"%s\"" % cmd if conf.windows else cmd + " &")
    except: log.ERR("Command not found:", cmd)










# Open help browser, streamtuner2 pages

def help(*args):
    run("yelp /usr/share/doc/streamtuner2/help/")


# Invokes player/recorder for stream url and format

def run_fmt_url(row={}, audioformat="audio/mpeg", source="pls", url=None, assoc={}):
    if not url:
        url = row["url"]
    if audioformat in handler:
        handler[audioformat](row, audioformat, source, url, assoc)
    else:
        cmd = mime_app(audioformat, assoc)
        cmd = interpol(cmd, url, source, row)
        run(cmd)

# Start web browser
def browser(url):
    run_fmt_url({}, "url/http", "srv", url, conf.play)

# Calls player for stream url and format
def play(row={}, audioformat="audio/mpeg", source="pls", url=None):
    run_fmt_url(row, audioformat, source, url, conf.play)

# Call streamripper / youtube-dl / wget
def record(row={}, audioformat="audio/mpeg", source="href", url=None):
    run_fmt_url(row, audioformat, source, url, conf.record)




# OS shell command escaping
#
def quote(ins):
    if type(ins) is list:
        return " ".join(["%r" % str(s) for s in ins])

Deleted contrib/cfg_soundcloud.py version [1301f5ed6f].

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
# api: streamtuner2
# title: Soundcloud player
# description: Just sets a new configuration option for `soundcli`
# version: -1
# url: http://elephly.net/soundcli.html
# priority: once
# type: config
# category: player
# 
# You only need to run this plugin once. It just adds an
# entry for "audio/soundcloud" in the player config list.

from config import *

# just once
class cfg_soundcloud(object):

    module = "cfg_soundcloud"
    fmt = "audio/soundcloud"
    cmd = "xterm -e \"soundcli stream %srv\""

    def __init__(self, *a, **kw):
        conf.play.setdefault(self.fmt, self.cmd)
        print self.module
        conf.plugins[self.module] = False


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






















































Modified contrib/reddit.py from [74725ffeda] to [46f1758ac3].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# encoding: UTF-8
# api: streamtuner2
# title: redditβ›±
# description: Music recommendations from reddit /r/music and associated subreddits.
# version: 0.8
# type: channel
# url: http://reddit.com/r/Music
# category: playlist
# config:
#   { name: reddit_pages, type: int, value: 2, description: Number of pages to fetch. }
#   { name: kill_soundcloud, type: boolean, value: 1, description: Filter soundcloud/spotify/etc if there's no player configured. }
#   { name: reddit_keep_all, type: boolean, value: 0, description: Keep all web links (starts a browser for websites/news). }
# png:
#   iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJ1BMVEUAAAAcICX/AABHSk1jZ299hYz/bmajq6//lY/d0M3C1+3T7P38+/iaLhuGAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgF
#   HUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQffBRUXIyQbWArCAAAAh0lEQVQI12Pg3g0BDLtXrVq1eveq3Qy7gIxCU9dqEGO11/ZKbzBDenUIUM3u7cGi1UDFW0TE55wsdpZikAw/
#   eebMnMmHGVxqDuUc0zzpynD4zIk5J3vOSDNsOQMG1gy7bI5HTq85Ws2wu/jM9PIzrkArdhmXlzuuXg00eVd5+epVqxmgrtgNAOWeS1KYtcY4AAAAAElFTkSuQmCC
# priority: extra
#










|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# encoding: UTF-8
# api: streamtuner2
# title: redditβ›±
# description: Music recommendations from reddit /r/music and associated subreddits.
# version: 0.8
# type: channel
# url: http://reddit.com/r/Music
# category: playlist
# config:
#   { name: reddit_pages, type: int, value: 2, description: Number of pages to fetch. }
#   { name: filter_walledgardens, type: boolean, value: 1, description: Filter walled gardens (soundcloud/spotify/…) if there's no player. }
#   { name: reddit_keep_all, type: boolean, value: 0, description: Keep all web links (starts a browser for websites/news). }
# png:
#   iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJ1BMVEUAAAAcICX/AABHSk1jZ299hYz/bmajq6//lY/d0M3C1+3T7P38+/iaLhuGAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgF
#   HUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQffBRUXIyQbWArCAAAAh0lEQVQI12Pg3g0BDLtXrVq1eveq3Qy7gIxCU9dqEGO11/ZKbzBDenUIUM3u7cGi1UDFW0TE55wsdpZikAw/
#   eebMnMmHGVxqDuUc0zzpynD4zIk5J3vOSDNsOQMG1gy7bI5HTq85Ws2wu/jM9PIzrkArdhmXlzuuXg00eVd5+epVqxmgrtgNAOWeS1KYtcY4AAAAAElFTkSuQmCC
# priority: extra
#
31
32
33
34
35
36
37

38
39
40
41
42
43
44
# is not very enticing.


import json
import re
from config import *
from channels import *

import ahttp


# reddit.com
#
# Uses old API requests such as:
#  β†’ http://www.reddit.com/r/music/new.json?sort=new







>







31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# is not very enticing.


import json
import re
from config import *
from channels import *
import action
import ahttp


# reddit.com
#
# Uses old API requests such as:
#  β†’ http://www.reddit.com/r/music/new.json?sort=new
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
            elif text_urls:
                row["url"] = text_urls[0]
                format = "video/youtube"
            # check for specific web links (Soundcloud etc.)
            else:
                listformat = "srv"
                format = None


                for urltype in ("soundcloud", "spotify", "bandcamp", "mixcloud"):
                    if row["url"].find(urltype) > 0:
                        # is a specific player configured?
                        fmt = "audio/" + urltype
                        if fmt in conf.play:
                            state = "gtk-media-forward"
                            format = fmt
                        # retain it as web link?
                        elif not conf.kill_soundcloud:
                            state = "gtk-media-pause"
                            format = "url/http"
                        break
                # else skip entry completely
                if not format:
                    if conf.reddit_keep_all:
                        state = "gtk-page-setup"
                        format = "url/http"
                    else:
                        log.DATA_SKIP(format, row["url"])







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







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
            elif text_urls:
                row["url"] = text_urls[0]
                format = "video/youtube"
            # check for specific web links (Soundcloud etc.)
            else:
                listformat = "srv"
                format = None
                # look for walled gardens
                urltype = re.findall("([\w-]+)\.\w+/", row["url"] + "/x-unknown.com/")[0]
                if urltype in ("soundcloud", "spotify", "bandcamp", "mixcloud"):

                    # is a specific player configured?
                    fmt = "audio/" + urltype
                    if fmt in conf.play or fmt in action.handler:
                        state = "gtk-media-forward"
                        format = fmt
                    # retain it as web link?
                    elif not conf.filter_walledgardens:
                        state = "gtk-media-pause"
                        format = "url/http"

                # else skip entry completely
                if not format:
                    if conf.reddit_keep_all:
                        state = "gtk-page-setup"
                        format = "url/http"
                    else:
                        log.DATA_SKIP(format, row["url"])

Added contrib/url_soundcloud.py version [18282e867b].





























































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
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
# api: streamtuner2
# title: Soundcloud streams
# description: Convert soundcloud links from reddit to streamable tracks
# version: 0.1
# type: filter
# category: audio
# depends: python:soundcloud, action >= 1.0, reddit >= 0.6
# priority: rare
#
# Overrides action.play() function to convert soundcloud URLs
# to track/streaming address.  Disables the reddit filter for
# walled gardens, and overrides any custom player configured
# for "audio/soundcloud" in settings.


import re
import soundcloud
from config import *
import ahttp
import action

fmt = "audio/soundcloud"
rx_url = re.compile("^https?://(www\.)?soundcloud\.com/[\w-]+/[\w-]+$")
conn = None

        
# API connect
def client():
    global conn
    if not conn:
        conn = soundcloud.Client(client_id="f0aea6e0484043f6638cb5bf35d43312")
    return conn

# Capture play events for faux MIME type
def sndcl_convert(row={}, audioformat="audio/mpeg", source="pls", url=None, assoc={}):
    if audioformat==fmt or rx_url.match(url):

        # find streaming address
        try:
            log.DATA_CONVERT_SOUNDCLOUD(url)
            track = client().get('/resolve', url=url)
            track_str = "/tracks/{}/stream".format(track.id)
            url = client().get(track_str, allow_redirects=False).location

            # override attributes
            row["url"] = url
            source = "srv"
            audioformat = "audio/mpeg"

        except Exception as e:
            log.ERR_SOUNDCLOUD("URL resolving failed:", e)
            
            # let web browser run
            audioformat = "url/http"
    
    # let primary handler take over
    if audioformat != fmt:
        return action.run_fmt_url(row, audioformat, source, url, assoc)


# Hook up custom action.handler for soundcloud URLs
#
# Still somewhat hodgepodge. The action module just lets .play() params
# rewrite by above handler. Should turn faux "audio/soundcloud" URL into
# plain/longwinded MP3 streaming address.
#
# Would need more generalized processing of custom URL schemes. But so
# far only the reddit module uses them anyway.
#
class url_soundcloud(object):
    module = "url_soundcloud"

    # override action.play() with wrapper
    def __init__(self, parent, *a, **kw):
        conf.play[fmt] = "false / convert"
        #conf.filter_walledgardens = False
        action.handler[fmt] = sndcl_convert