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

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


Check-in [328c3ac5fa]

Overview
Comment:Move and delete delicast/streamlicensing channels.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 328c3ac5faeaf64714cde3b2853b86b482f4348c
User & Date: mario on 2020-05-11 18:08:43
Other Links: manifest | tags
Context
2020-05-11
18:16
Add pq.find() and log.ERR to `pq` wrapper module. check-in: b7e5457c6e user: mario tags: trunk
18:08
Move and delete delicast/streamlicensing channels. check-in: 328c3ac5fa user: mario tags: trunk
18:07
Delicast and Streamlicensing went offline. check-in: d3bb740f0b user: mario tags: trunk
Changes

Deleted contrib/delicast.py version [b4cf3b41af].

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
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
# encoding: UTF-8
# api: streamtuner2
# title: Delicast
# description: directory of streaming media
# url: http://delicast.com/
# version: 0.8
# type: channel
# category: radio
# priority: obsolete
# config: -
# png:
#    iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAA
#    AmJLR0QA/4ePzL8AAAAHdElNRQffBB4UJAsX77G0AAAANUlEQVQY02OwQwMMdv/BAEUASCFEoAIIEZIEIGYjBCAUwpb/6O5ACEABGQJ2cFsQIlB3oAEA6iVo+vl+BbQA
#    AAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMDQtMzBUMjI6MzY6MDMrMDI6MDAFLUvfAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTA0LTMwVDIyOjM2OjAzKzAyOjAwdHDz
#    YwAAAABJRU5ErkJggg==
# priority: rare
# extraction-method: regex, action-handler
#
# Just a standard genre/country radio directory. Not very
# suitable for extraction actually, because it requires a
# second page request for uncovering the streaming URLs.
#
# Audio URL lookup is done in urn_resolve action handler now,
# so only happens on playback. Which of course won't allow for
# exporting/bookmarking etc.
# Now fetches up to 5 pages (20 entries each).


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


# Delayed streaming URL discovery
class delicast (ChannelPlugin):

    # control flags
    has_search = False
    listformat = "srv"
    audioformat = "audio/mpeg"
    titles = dict(listeners=False, bitrate=False, playing="Location")

    categories = ["60s", "70s", "80s", "90s", "Alternative", "Blues",
    "Chillout", "Christian", "Classical", "Community", "Country", "Culture",
    "Dance", "Disco", "Easy listening", "Electronic", "Folk", "Funk",
    "Gospel", "Hiphop", "House Indie", "Information", "Jazz", "Latin",
    "Lounge", "Love", "Metal", "Oldies", "Pop", "R n b", "Reggae", "Rock",
    "Romantic", "Soul", "Sports", "Student", "Talk", "Techno", "Trance",
    "Urban", "World music"]
    

    # static
    def update_categories(self):
        pass


    # Fetch entries
    def update_streams(self, cat, search=None):

        ucat = re.sub("\W+", "-", cat.lower())
        html = ""
        for i in range(1, 5):
            add = ahttp.get("http://delicast.com/radio/q:" + ucat + ("" if i == 1 else "/%s" % i))
            html += add
            if not re.search("href='http://delicast.com/radio/q:%s/%s'" % (ucat, i+1), add):
                break
        r = []
        log.HTML(html)
        for ls in re.findall("""
                <b>\d+</b>\.
                .*?
                <a[^>]+href="(http[^"]+/radio/\w+)"
                .*?
                /pics/((?!play_tri)\w+)
                .*?
                120%'>([^<>]+)</span>
            """, html, re.X|re.S):
            if len(ls):
                homepage, country, title = ls
                r.append(dict(
                    homepage = homepage,
                    playing = country,
                    title = unhtml(title),
                    url = "urn:delicast",
                    genre = cat,
             #      genre = unhtml(tags),
                ))
        return r
      

    # Update `url` on station data access (incurs a delay for playing or recording)
    def resolve_urn(self, row):
        if row.get("url").startswith("urn:delicast"):
            html = ahttp.get(row["homepage"])
            ls = re.findall("^var url = \"(.+)\";", html, re.M)
            if ls:
                row["url"] = unhtml(ls[0])
            else:
                log.ERR("No stream found on %s" % row["homepage"])
        return row

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














































































































































































































Deleted contrib/streamlicensing.py version [566d465d6d].

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
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
# encoding: UTF-8
# api: streamtuner2
# title: streamlicensing
# description: Smaller streaming service provider
# url: http://www.streamlicensing.com/directory/
# version: 0.2
# type: channel
# category: radio
# priority: obsolete
# png:
#   iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAGFBMVEVhcAAODy8rLVpHS4RPU22DismUmLTm6va3Zc/ZAAAAAXRSTlMAQObYZgAA
#   AAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfhAgwUMiN14qDwAAAAW0lEQVQI12NggAADCMVW7gym3crBDJb08rJgZbB4
#   qqEQmBGsKMQEZggqMLCA1LAJM8AZaWUQhjpEO5uwamiQMJjBrChkADSlXBhsfHh5qgCYoWysqACxWQlCAwArBw5QNfhFygAAAABJRU5ErkJggg==
# extraction-method: regex
#
# Streaming service provider, which ensures station legality and fees
# in accordance with US copyright conversion corporations.
#
# Has only major categories. Does not provide channel homepages, and
# is a bit slow due to huge page sizes. No search function implemented
# here.
#


import re
import ahttp
from config import *
from channels import *


# streamlicensing.com
#
# ยท Provides around 20 categories, associated to numeric ids (?g= parameter)
#
# ยท Station lists are one <tr> block each, with a JS inline script associating
#   some web player parameters.
#
# ยท Each station has a station id=, but also a stream_id= for the playlist
#   generator.
#
# ยท Normally just provides a web player, but used to show .pls links for various
#   players. Meanwhile this is hidden, but the playlist generator is still
#   available - so doesn't require double lookups.
#   โ†’ http://www.streamlicensing.com/directory/index.cgi?action=webpro_links&sid=4785&start=1&g=14&e=1&s=
#   โ†’ .../directory/index.cgi/playlist.pls?action=playlist&type=pls&sid=4785&stream_id=1234
#
# ยท family_safe and maxpages are hardcoded config options for now.
#
class streamlicensing (ChannelPlugin):

    # module attributes
    listformat = "pls"
    has_search = False
    categories = []
    catmap = {}
    titles = dict( listeners=False )
    
    # config (not as plugin options here)
    conf_family_unsafe = 0
    conf_maxpages = max(int(int(conf.max_streams) / 100), 1)
    
    # magic values
    base_url = "http://www.streamlicensing.com/directory/"
    pls_sffx = "%sindex.cgi/playlist.pls?action=playlist&type=pls&sid=%s&stream_id=%s"


    # fetch category titles and catmap
    def update_categories(self):
        html = ahttp.get(self.base_url)
        cats = re.findall('"\?start=&g=(\d+)&e=&s="><.+?>([\w\s-]+)</span>', html)
        self.categories = sorted([c[1] for c in cats])
        self.catmap = dict([(t,i) for i,t in cats])


    # extract stream urls
    def update_streams(self, cat):
    
        # prep block regex
        rx_genre = re.compile(r"""
           <tr\sid='(\d+)' .*?
           Station\sName:<.*?>([^<]+)</(?:span|font|td|a)> .*?
           ^var\slastsong_\d+\s*=\s*'([^\n]+)'; .*?
           <a[^>]+onClick=[^>]+&stream_id=(\d+)'[^>]+>(\d+)k<
        """, re.I|re.S|re.X|re.M)
        
        # collect pages into single string
        html = ""
        for page in range(0, self.conf_maxpages):
            self.progress(self.conf_maxpages, page)
            html += ahttp.get("%s?start=%s&g=%s&e=%s&s=" % (self.base_url, page * 10, self.catmap[cat], self.conf_family_unsafe))
            if not re.search("\?start=%s.*>Next" % ((page + 1) * 10), html):
                break
        html = re.sub(">Featured Stations.+?>Previous Page", "", html, 100, re.S)

        # extract and convert to station rows
        entries = []
        for uu in re.findall(rx_genre, html):
            print uu
            entries.append(dict(
                genre = cat,
                id = to_int(uu[0]),
                sid = to_int(uu[3]),
                title = unhtml(uu[1]),
                playing = unhtml(uu[2]),  # actually JS decoding...
                format = "audio/mpeg",
                bitrate = to_int(uu[4]),
                url = self.pls_sffx % (self.base_url, uu[0], uu[3])
            ))
        return entries

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