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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [fff3b62827]

Overview
Comment:Completer plugin meta data blocks, now utilized in channel settings dialog for nicer descriptions.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: fff3b62827ca4703be7a50f5660c2118a7f1f588
User & Date: mario on 2014-05-13 16:23:02
Other Links: manifest | tags
Context
2014-05-13
18:16
Fix missing mygtk import check-in: 2ccad59967 user: mario tags: trunk
16:23
Completer plugin meta data blocks, now utilized in channel settings dialog for nicer descriptions. check-in: fff3b62827 user: mario tags: trunk
03:26
Make config.pyquery and config.debug global options. check-in: 3b842c85a3 user: mario tags: trunk
Changes

Modified channels/__init__.py from [92d73c2e69] to [1d725c2e92].

1
2



3
4


5
6
7





8













































#
# encoding: UTF-8



# api: python
# type: R


#







from channels._generic import *















































>
>
>


>
>

|
|
>
>
>
>
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
#
# encoding: UTF-8
# api: streamtuner2
# title: Plugin handling
# description: Channels and feature plugins reside in channels/
# api: python
# type: R
# category: core
# priority: core
#
#
#
#
#
#


from channels._generic import *

# Only reexport plugin classes
__all__ = [
    "GenericChannel", "ChannelPlugin"
]



# Search through ./channels/ and get module basenames.
# Also order them by conf.channel_order
#
def module_list():

    # find plugin files
    ls = os.listdir(conf.share + "/channels/")
    ls = [fn[:-3] for fn in ls if re.match("^[a-z][\w\d_]+\.py$", fn)]
    
    # resort with tab order
    order = [module.strip() for module in conf.channel_order.lower().replace(".","_").replace("-","_").split(",")]
    ls = [module for module in (order) if (module in ls)] + [module for module in (ls) if (module not in order)]

    return ls


# Parse plugin comment blocks.
#
def module_meta():
    meta = {}

    rx_meta = re.compile(r"""^#\s*(\w+):\s*(.+)$""", re.M)

    # Loop through all existing module.py scripts
    for name in module_list():
       meta[name] = dict(title="", type="", description="")

       # Read and regex-extract into dict
       with open("%s/channels/%s.py" % (conf.share, name)) as f:
           for field in re.findall(rx_meta, f.read(1024)):
               meta[name][field[0]] = field[1]

    return meta




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

1
2
3
4

5
6
7
8
9

10
11
12
13
14
15
16
#
# encoding: UTF-8
# api: streamtuner2
# type: class

# title: channel objects
# description: base functionality for channel modules
# version: 1.0
# author: mario
# license: public domain

#
#
#  GenericChannel implements the basic GUI functions and defines
#  the default channel data structure. It implements base and
#  fallback logic for all other channel implementations.
#
#  Built-in channels derive directly from generic. Additional



|
>


|


>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#
# encoding: UTF-8
# api: streamtuner2
# type: internal
# category: ui
# title: channel objects
# description: base functionality for channel modules
# version: 1.1
# author: mario
# license: public domain
# priority: core
#
#
#  GenericChannel implements the basic GUI functions and defines
#  the default channel data structure. It implements base and
#  fallback logic for all other channel implementations.
#
#  Built-in channels derive directly from generic. Additional
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
        pass


# generic channel module                            ---------------------------------------
class GenericChannel(object):

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








<


<







43
44
45
46
47
48
49

50
51

52
53
54
55
56
57
58
        pass


# generic channel module                            ---------------------------------------
class GenericChannel(object):

        # desc

        module = "generic"
        title = "GenericChannel"

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

Modified channels/file.py from [13559afbce] to [a8e2e93dda].

1
2
3
4


5

6
7
8
9
10
11
12
13
14
15
16
17
#
# api: streamtuner2
# title: file browser plugin
# description: browses through local files, displays mp3/oggs and m3u/pls files


# version: -0.5

# depends: mutagen, kiwi
# x:
# x:
# x:
# x:
#
#
# Local file browser.
#
#




|
|
>
>
|
>

<
<
<
<







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




#
#
# Local file browser.
#
#


Modified channels/global_key.py from [3bfb69eda6] to [3a244b8b88].

1
2
3
4
5


6

7
8
9
10
11
12
13
#
# type: feature
# api: streamtuner2
# title: global keyboard shortcut
# description: allows switching radios in bookmarks list via key press


# version: 0.2

# depends: python-keybinder
#
#
# Binds a key to global desktop (F13 = left windows key). On keypress
# it switches the currently playing radio station to another one in
# bookmarks list.
#

<

|
|
>
>

>







1

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

# api: streamtuner2
# title: Global keyboard shortcut
# description: Allows switching between bookmarked radios via key press.
# type: feature
# category: ui
# version: 0.2
# priority: extra
# depends: python-keybinder
#
#
# Binds a key to global desktop (F13 = left windows key). On keypress
# it switches the currently playing radio station to another one in
# bookmarks list.
#
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37


# register a key
class global_key(object):

    module = "global_key"
    title = "keyboard shortcut"
    version = 0.2
    
    config = [
        dict(name="switch_key", type="text", value="XF86Forward", description="global key for switching radio"),
        dict(name="switch_channel", type="text", value="bookmarks:favourite", description="station list to alternate in"),
        dict(name="switch_random", type="boolean", value=0, description="pick random channel, instead of next"),
    ]
    last = 0







<







25
26
27
28
29
30
31

32
33
34
35
36
37
38


# register a key
class global_key(object):

    module = "global_key"
    title = "keyboard shortcut"

    
    config = [
        dict(name="switch_key", type="text", value="XF86Forward", description="global key for switching radio"),
        dict(name="switch_channel", type="text", value="bookmarks:favourite", description="station list to alternate in"),
        dict(name="switch_random", type="boolean", value=0, description="pick random channel, instead of next"),
    ]
    last = 0

Modified channels/google.py from [40a11acd81] to [67db911f9e].

1
2
3
4
5
6


7

8
9
10
11
12
13
14
#
# encoding: ISO-8859-1
# api: streamtuner2
# title: google stations
# description: Looks up web radio stations from DMOZ/Google directory
# depends: channels, re, http


# version: 0.1

# author: Mario, original: Jean-Yves Lefort
#
# This is a plugun from streamtuner1. It has been rewritten for the
# more mundane plugin API of streamtuner2 - reimplementing ST seemed
# to much work.
# Also it has been rewritten to query DMOZ directly. Google required
# the use of fake User-Agents for access, and the structure on DMOZ



|
|
|
>
>
|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#
# encoding: ISO-8859-1
# api: streamtuner2
# title: Google stations
# description: Looks up web radio homepages from DMOZ/Google directory.
# type: channel
# category: web
# priority: deprecated
# version: 0.2
# depends: channels, re, http
# author: Mario, original: Jean-Yves Lefort
#
# This is a plugun from streamtuner1. It has been rewritten for the
# more mundane plugin API of streamtuner2 - reimplementing ST seemed
# to much work.
# Also it has been rewritten to query DMOZ directly. Google required
# the use of fake User-Agents for access, and the structure on DMOZ
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# Google Stations is actually now DMOZ Stations
class google(ChannelPlugin):

    # description
    title = "Google"
    module = "google"
    homepage = GOOGLE_STATIONS_HOME
    version = 0.2
    
    # config data
    config = [
#        {"name": "theme", "type": "text", "value":"Tactile", "description":"Streamtuner2 theme; no this isn't a google-specific option. But you know, the plugin options are a new toy."},
#        {"name": "flag2", "type": "boolean", "value":1, "description":"oh see, an unused checkbox"}
    ]
    







<







81
82
83
84
85
86
87

88
89
90
91
92
93
94
# Google Stations is actually now DMOZ Stations
class google(ChannelPlugin):

    # description
    title = "Google"
    module = "google"
    homepage = GOOGLE_STATIONS_HOME

    
    # config data
    config = [
#        {"name": "theme", "type": "text", "value":"Tactile", "description":"Streamtuner2 theme; no this isn't a google-specific option. But you know, the plugin options are a new toy."},
#        {"name": "flag2", "type": "boolean", "value":1, "description":"oh see, an unused checkbox"}
    ]
    

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

1
2
3
4


5

6
7
8
9
10
11
12
#
# api: streamtuner2
# title: internet-radio.org.uk
# description: io channel


# version: 0.1

#
#
# Might become new main plugin
#
#
#



|
|
>
>

>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#
# api: streamtuner2
# title: Internet-Radio.org.uk
# description: Broad list of webradios from all genres.
# type: channel
# category: radio
# version: 0.1
# priority: standard
#
#
# Might become new main plugin
#
#
#

25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class internet_radio_org_uk (ChannelPlugin):


    # description
    title = "InternetRadio"
    module = "internet_radio_org_uk"
    homepage = "http://www.internet-radio.org.uk/"
    version = 0.1
    listformat = "audio/x-scpls"
    
    # settings
    config = [
        {"name":"internetradio_max_pages", "type":"int", "value":5, "description":"How many pages to fetch and read."},
    ]
    







<







28
29
30
31
32
33
34

35
36
37
38
39
40
41
class internet_radio_org_uk (ChannelPlugin):


    # description
    title = "InternetRadio"
    module = "internet_radio_org_uk"
    homepage = "http://www.internet-radio.org.uk/"

    listformat = "audio/x-scpls"
    
    # settings
    config = [
        {"name":"internetradio_max_pages", "type":"int", "value":5, "description":"How many pages to fetch and read."},
    ]
    

Modified channels/jamendo.py from [d8e3335952] to [5fc1148fae].

1
2
3
4


5

6
7
8
9
10
11
12

# api: streamtuner2
# title: jamendo browser
# description: Jamendo is a license-free music collection and artist hub


# depends: json

#
# Now utilizes the Jamendo /v3.0/ API.
#
# Radio station lists are fixed for now. Querying the API twice per station
# doesn't seem overly sensible.
#
# Albums and Playlists are limited to 200 entries. Adding a cursor is


|
|
>
>

>







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

# api: streamtuner2
# title: Jamendo
# description: A license-free music collection and artist hub.
# type: channel
# category: radio
# depends: json
# priority: default
#
# Now utilizes the Jamendo /v3.0/ API.
#
# Radio station lists are fixed for now. Querying the API twice per station
# doesn't seem overly sensible.
#
# Albums and Playlists are limited to 200 entries. Adding a cursor is

Modified channels/links.py from [e2e193464a] to [a17530466f].

1
2
3
4


5
6
7
8
9
10
11
12
13
#
# api: streamtuner2
# title: links to directory services
# description: provides a simple list of homepages for directory services


# version: 0.1
# priority: rare
#
#
# Simply adds a "links" entry in bookmarks tab, where known channels
# and some others are listed with homepage links.
#
#



|
|
>
>

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#
# api: streamtuner2
# title: Links to directory services
# description: Static list of various music directory websites.
# type: category
# category: web
# version: 0.1
# priority: default
#
#
# Simply adds a "links" entry in bookmarks tab, where known channels
# and some others are listed with homepage links.
#
#

Modified channels/live365.py from [10562bd5f9] to [cd7a2a9078].


1
2





3
4
5
6
7
8
9

# api: st2
# title: live365 channel





#
# 2.0.9 fixed by Abhisek Sanyal
#




>
|
|
>
>
>
>
>







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

# api: streamtunter2
# title: Live365
# description: Around 5000 categorized internet radio streams, some paid ad-free ones.
# type: channel
# category: radio
# version: 0.2
# priority: optional
#
# 2.0.9 fixed by Abhisek Sanyal
#




24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41


# channel live365
class live365(ChannelPlugin):


        # desc
        api = "streamtuner2"
        module = "live365"
        title = "Live365"
        version = 0.1
        homepage = "http://www.live365.com/"
        base_url = "http://www.live365.com/"
        listformat = "url/http"
        mediatype = "audio/mpeg"

        # content
        categories = ['Alternative', ['Britpop', 'Classic Alternative', 'College', 'Dancepunk', 'Dream Pop', 'Emo', 'Goth', 'Grunge', 'Indie Pop', 'Indie Rock', 'Industrial', 'Lo-Fi', 'Modern Rock', 'New Wave', 'Noise Pop', 'Post-Punk', 'Power Pop', 'Punk'], 'Blues', ['Acoustic Blues', 'Chicago Blues', 'Contemporary Blues', 'Country Blues', 'Delta Blues', 'Electric Blues', 'Cajun/Zydeco'], 'Classical', ['Baroque', 'Chamber', 'Choral', 'Classical Period', 'Early Classical', 'Impressionist', 'Modern', 'Opera', 'Piano', 'Romantic', 'Symphony'], 'Country', ['Alt-Country', 'Americana', 'Bluegrass', 'Classic Country', 'Contemporary Bluegrass', 'Contemporary Country', 'Honky Tonk', 'Hot Country Hits', 'Western'], 'Easy Listening', ['Exotica', 'Lounge', 'Orchestral Pop', 'Polka', 'Space Age Pop'], 'Electronic/Dance', ['Acid House', 'Ambient', 'Big Beat', 'Breakbeat', 'Disco', 'Downtempo', "Drum 'n' Bass", 'Electro', 'Garage', 'Hard House', 'House', 'IDM', 'Jungle', 'Progressive', 'Techno', 'Trance', 'Tribal', 'Trip Hop'], 'Folk', ['Alternative Folk', 'Contemporary Folk', 'Folk Rock', 'New Acoustic', 'Traditional Folk', 'World Folk'], 'Freeform', ['Chill', 'Experimental', 'Heartache', 'Love/Romance', 'Music To ... To', 'Party Mix', 'Patriotic', 'Rainy Day Mix', 'Reality', 'Shuffle/Random', 'Travel Mix', 'Trippy', 'Various', 'Women', 'Work Mix'], 'Hip-Hop/Rap', ['Alternative Rap', 'Dirty South', 'East Coast Rap', 'Freestyle', 'Gangsta Rap', 'Old School', 'Turntablism', 'Underground Hip-Hop', 'West Coast Rap'], 'Inspirational', ['Christian', 'Christian Metal', 'Christian Rap', 'Christian Rock', 'Classic Christian', 'Contemporary Gospel', 'Gospel', 'Praise/Worship', 'Sermons/Services', 'Southern Gospel', 'Traditional Gospel'], 'International', ['African', 'Arabic', 'Asian', 'Brazilian', 'Caribbean', 'Celtic', 'European', 'Filipino', 'Greek', 'Hawaiian/Pacific', 'Hindi', 'Indian', 'Japanese', 'Jewish', 'Mediterranean', 'Middle Eastern', 'North American', 'Soca', 'South American', 'Tamil', 'Worldbeat', 'Zouk'], 'Jazz', ['Acid Jazz', 'Avant Garde', 'Big Band', 'Bop', 'Classic Jazz', 'Cool Jazz', 'Fusion', 'Hard Bop', 'Latin Jazz', 'Smooth Jazz', 'Swing', 'Vocal Jazz', 'World Fusion'], 'Latin', ['Bachata', 'Banda', 'Bossa Nova', 'Cumbia', 'Latin Dance', 'Latin Pop', 'Latin Rap/Hip-Hop', 'Latin Rock', 'Mariachi', 'Merengue', 'Ranchera', 'Salsa', 'Tango', 'Tejano', 'Tropicalia'], 'Metal', ['Extreme Metal', 'Heavy Metal', 'Industrial Metal', 'Pop Metal/Hair', 'Rap Metal'], 'New Age', ['Environmental', 'Ethnic Fusion', 'Healing', 'Meditation', 'Spiritual'], 'Oldies', ['30s', '40s', '50s', '60s', '70s', '80s', '90s'], 'Pop', ['Adult Contemporary', 'Barbershop', 'Bubblegum Pop', 'Dance Pop', 'JPOP', 'Soft Rock', 'Teen Pop', 'Top 40', 'World Pop'], 'R&B/Urban', ['Classic R&B', 'Contemporary R&B', 'Doo Wop', 'Funk', 'Motown', 'Neo-Soul', 'Quiet Storm', 'Soul', 'Urban Contemporary'], 'Reggae', ['Contemporary Reggae', 'Dancehall', 'Dub', 'Pop-Reggae', 'Ragga', 'Reggaeton', 'Rock Steady', 'Roots Reggae', 'Ska'], 'Rock', ['Adult Album Alternative', 'British Invasion', 'Classic Rock', 'Garage Rock', 'Glam', 'Hard Rock', 'Jam Bands', 'Prog/Art Rock', 'Psychedelic', 'Rock & Roll', 'Rockabilly', 'Singer/Songwriter', 'Surf'], 'Seasonal/Holiday', ['Anniversary', 'Birthday', 'Christmas', 'Halloween', 'Hanukkah', 'Honeymoon', 'Valentine', 'Wedding'], 'Soundtracks', ['Anime', "Children's/Family", 'Original Score', 'Showtunes'], 'Talk', ['Comedy', 'Community', 'Educational', 'Government', 'News', 'Old Time Radio', 'Other Talk', 'Political', 'Scanner', 'Spoken Word', 'Sports']]







<


<







30
31
32
33
34
35
36

37
38

39
40
41
42
43
44
45


# channel live365
class live365(ChannelPlugin):


        # desc

        module = "live365"
        title = "Live365"

        homepage = "http://www.live365.com/"
        base_url = "http://www.live365.com/"
        listformat = "url/http"
        mediatype = "audio/mpeg"

        # content
        categories = ['Alternative', ['Britpop', 'Classic Alternative', 'College', 'Dancepunk', 'Dream Pop', 'Emo', 'Goth', 'Grunge', 'Indie Pop', 'Indie Rock', 'Industrial', 'Lo-Fi', 'Modern Rock', 'New Wave', 'Noise Pop', 'Post-Punk', 'Power Pop', 'Punk'], 'Blues', ['Acoustic Blues', 'Chicago Blues', 'Contemporary Blues', 'Country Blues', 'Delta Blues', 'Electric Blues', 'Cajun/Zydeco'], 'Classical', ['Baroque', 'Chamber', 'Choral', 'Classical Period', 'Early Classical', 'Impressionist', 'Modern', 'Opera', 'Piano', 'Romantic', 'Symphony'], 'Country', ['Alt-Country', 'Americana', 'Bluegrass', 'Classic Country', 'Contemporary Bluegrass', 'Contemporary Country', 'Honky Tonk', 'Hot Country Hits', 'Western'], 'Easy Listening', ['Exotica', 'Lounge', 'Orchestral Pop', 'Polka', 'Space Age Pop'], 'Electronic/Dance', ['Acid House', 'Ambient', 'Big Beat', 'Breakbeat', 'Disco', 'Downtempo', "Drum 'n' Bass", 'Electro', 'Garage', 'Hard House', 'House', 'IDM', 'Jungle', 'Progressive', 'Techno', 'Trance', 'Tribal', 'Trip Hop'], 'Folk', ['Alternative Folk', 'Contemporary Folk', 'Folk Rock', 'New Acoustic', 'Traditional Folk', 'World Folk'], 'Freeform', ['Chill', 'Experimental', 'Heartache', 'Love/Romance', 'Music To ... To', 'Party Mix', 'Patriotic', 'Rainy Day Mix', 'Reality', 'Shuffle/Random', 'Travel Mix', 'Trippy', 'Various', 'Women', 'Work Mix'], 'Hip-Hop/Rap', ['Alternative Rap', 'Dirty South', 'East Coast Rap', 'Freestyle', 'Gangsta Rap', 'Old School', 'Turntablism', 'Underground Hip-Hop', 'West Coast Rap'], 'Inspirational', ['Christian', 'Christian Metal', 'Christian Rap', 'Christian Rock', 'Classic Christian', 'Contemporary Gospel', 'Gospel', 'Praise/Worship', 'Sermons/Services', 'Southern Gospel', 'Traditional Gospel'], 'International', ['African', 'Arabic', 'Asian', 'Brazilian', 'Caribbean', 'Celtic', 'European', 'Filipino', 'Greek', 'Hawaiian/Pacific', 'Hindi', 'Indian', 'Japanese', 'Jewish', 'Mediterranean', 'Middle Eastern', 'North American', 'Soca', 'South American', 'Tamil', 'Worldbeat', 'Zouk'], 'Jazz', ['Acid Jazz', 'Avant Garde', 'Big Band', 'Bop', 'Classic Jazz', 'Cool Jazz', 'Fusion', 'Hard Bop', 'Latin Jazz', 'Smooth Jazz', 'Swing', 'Vocal Jazz', 'World Fusion'], 'Latin', ['Bachata', 'Banda', 'Bossa Nova', 'Cumbia', 'Latin Dance', 'Latin Pop', 'Latin Rap/Hip-Hop', 'Latin Rock', 'Mariachi', 'Merengue', 'Ranchera', 'Salsa', 'Tango', 'Tejano', 'Tropicalia'], 'Metal', ['Extreme Metal', 'Heavy Metal', 'Industrial Metal', 'Pop Metal/Hair', 'Rap Metal'], 'New Age', ['Environmental', 'Ethnic Fusion', 'Healing', 'Meditation', 'Spiritual'], 'Oldies', ['30s', '40s', '50s', '60s', '70s', '80s', '90s'], 'Pop', ['Adult Contemporary', 'Barbershop', 'Bubblegum Pop', 'Dance Pop', 'JPOP', 'Soft Rock', 'Teen Pop', 'Top 40', 'World Pop'], 'R&B/Urban', ['Classic R&B', 'Contemporary R&B', 'Doo Wop', 'Funk', 'Motown', 'Neo-Soul', 'Quiet Storm', 'Soul', 'Urban Contemporary'], 'Reggae', ['Contemporary Reggae', 'Dancehall', 'Dub', 'Pop-Reggae', 'Ragga', 'Reggaeton', 'Rock Steady', 'Roots Reggae', 'Ska'], 'Rock', ['Adult Album Alternative', 'British Invasion', 'Classic Rock', 'Garage Rock', 'Glam', 'Hard Rock', 'Jam Bands', 'Prog/Art Rock', 'Psychedelic', 'Rock & Roll', 'Rockabilly', 'Singer/Songwriter', 'Surf'], 'Seasonal/Holiday', ['Anniversary', 'Birthday', 'Christmas', 'Halloween', 'Hanukkah', 'Honeymoon', 'Valentine', 'Wedding'], 'Soundtracks', ['Anime', "Children's/Family", 'Original Score', 'Showtunes'], 'Talk', ['Comedy', 'Community', 'Educational', 'Government', 'News', 'Old Time Radio', 'Other Talk', 'Political', 'Scanner', 'Spoken Word', 'Sports']]

Modified channels/modarchive.py from [a906bb560c] to [7c7eb2da06].

1
2
3





4
5
6
7
8
9
10

# api: streamtuner2
# title: modarchive browser





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


|
>
>
>
>
>







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

# api: streamtuner2
# title: MODarchive
# description: Collection of module / tracker audio files (MOD, S3M, XM, etc.)
# type: channel
# version: 0.1
# priority: extra
# category: media
#
#
# 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.
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# MODs
class modarchive (ChannelPlugin):

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

    # keeps category titles->urls    
    catmap = {}
    categories = []
 
    







<







39
40
41
42
43
44
45

46
47
48
49
50
51
52
# MODs
class modarchive (ChannelPlugin):

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

    base = "http://modarchive.org/"

    # keeps category titles->urls    
    catmap = {}
    categories = []
 
    

Modified channels/musicgoal.py from [487d9e3db3] to [3d5f5c99cc].

1
2
3
4


5

6
7
8
9
10
11
12
13
14
#
# api: streamtuner2
# title: MUSICGOAL channel
# description: musicgoal.com/.de combines radio and podcast listings


# version: 0.1

# status: experimental
# pre-config: <const name="api"/>
#
# Musicgoal.com is a radio and podcast directory. This plugin tries to use
# the new API for accessing listing data.
#
#




|
|
>
>

>

<







1
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16
#
# api: streamtuner2
# title: MUSICGOAL
# description: Broad list of radio stations and podcasts. Provides a sane API, but only 5 results each.
# type: channel
# category: radio
# version: 0.1
# priority: optional
# status: experimental

#
# Musicgoal.com is a radio and podcast directory. This plugin tries to use
# the new API for accessing listing data.
#
#


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
          
# I wonder what that is for                                             ---------------------------------------
class musicgoal (ChannelPlugin):

        # desc
        module = "musicgoal"
        title = "MUSICGOAL"
        version = 0.1
        homepage = "http://www.musicgoal.com/"
        base_url = homepage
        listformat = "url/direct"

        # settings
        config = [
        ]







<







30
31
32
33
34
35
36

37
38
39
40
41
42
43
          
# I wonder what that is for                                             ---------------------------------------
class musicgoal (ChannelPlugin):

        # desc
        module = "musicgoal"
        title = "MUSICGOAL"

        homepage = "http://www.musicgoal.com/"
        base_url = homepage
        listformat = "url/direct"

        # settings
        config = [
        ]

Modified channels/myoggradio.py from [68cf24bd7d] to [a4c03f3784].

1
2
3
4


5
6
7
8
9
10
11
12
13
14
15
16
#
# api: streamtuner2
# title: MyOggRadio channel plugin
# description: open source internet radio directory MyOggRadio


# version: 0.5
# config:
#    <var name="myoggradio_login" type="text" value="user:password" description="login account for myoggradio service" />
# priority: standard
# category: channel
# depends: json, StringIO
#
# MyOggRadio is an open source radio station directory. Because this matches
# 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.
#


|
|
>
>

<
<

<







1
2
3
4
5
6
7


8

9
10
11
12
13
14
15
#
# api: streamtuner2
# title: MyOggRadio
# description: Open source internet radio directory.
# type: channel
# category: radio
# version: 0.5


# priority: standard

# depends: json, StringIO
#
# MyOggRadio is an open source radio station directory. Because this matches
# 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.
#
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class myoggradio(ChannelPlugin):

    # description
    title = "MyOggRadio"
    module = "myoggradio"
    homepage = "http://www.myoggradio.org/"
    api = "http://ehm.homelinux.org/MyOggRadio/"
    version = 0.5
    listformat = "url/direct"
    
    # config data
    config = [
        {"name":"myoggradio_login", "type":"text", "value":"user:password", "description":"Account for storing personal favourites."},
        {"name":"myoggradio_morph", "type":"boolean", "value":0, "description":"Convert pls/m3u into direct shoutcast url."},
    ]







<







34
35
36
37
38
39
40

41
42
43
44
45
46
47
class myoggradio(ChannelPlugin):

    # description
    title = "MyOggRadio"
    module = "myoggradio"
    homepage = "http://www.myoggradio.org/"
    api = "http://ehm.homelinux.org/MyOggRadio/"

    listformat = "url/direct"
    
    # config data
    config = [
        {"name":"myoggradio_login", "type":"text", "value":"user:password", "description":"Account for storing personal favourites."},
        {"name":"myoggradio_morph", "type":"boolean", "value":0, "description":"Convert pls/m3u into direct shoutcast url."},
    ]

Modified channels/punkcast.py from [57433ea58f] to [e751e37b9b].

1
2
3





4
5
6
7
8
9
10

# api: streamtuner2
# title: punkcast listing





#
#
# Disables itself per default.
# ST1 looked prettier with random images within.
#




|
>
>
>
>
>







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

# api: streamtuner2
# title: PunkCast
# description: Online video site that covered NYC artists. Not updated anymore.
# type: channel
# category: video
# version: 0.1
# priority: rare
#
#
# Disables itself per default.
# ST1 looked prettier with random images within.
#


34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# basic.ch broadcast archive
class punkcast (ChannelPlugin):

    # description
    title = "punkcast"
    module = "punkcast"
    homepage = "http://www.punkcast.com/"
    version = 0.1

    # keeps category titles->urls    
    catmap = {}
    categories = ["list"]
    default = "list"
    current = "list"
 







<







39
40
41
42
43
44
45

46
47
48
49
50
51
52
# basic.ch broadcast archive
class punkcast (ChannelPlugin):

    # description
    title = "punkcast"
    module = "punkcast"
    homepage = "http://www.punkcast.com/"


    # keeps category titles->urls    
    catmap = {}
    categories = ["list"]
    default = "list"
    current = "list"
 

Modified channels/shoutcast.py from [19bd20260e] to [834f2c6518].

1
2
3
4
5


6

7
8
9
10
11
12
13
#
# api: streamtuner2
# title: shoutcast
# description: Channel/tab for Shoutcast.com directory
# depends: pq, re, http


# version: 1.3

# author: Mario
# original: Jean-Yves Lefort
#
# Shoutcast is a server software for audio streaming. It automatically spools
# station information on shoutcast.com, which this plugin can read out.
#
# After its recent aquisition the layout got slimmed down considerably. So


|
|
|
>
>

>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#
# api: streamtuner2
# title: Shoutcast.com
# description: Primary list of shoutcast servers (now managed by radionomy).
# type: channel
# category: radio
# priority: default
# version: 1.3
# depends: pq, re, http
# author: Mario
# original: Jean-Yves Lefort
#
# Shoutcast is a server software for audio streaming. It automatically spools
# station information on shoutcast.com, which this plugin can read out.
#
# After its recent aquisition the layout got slimmed down considerably. So
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# SHOUTcast data module                                          ----------------------------------------
class shoutcast(channels.ChannelPlugin):

        # desc
        api = "streamtuner2"
        module = "shoutcast"
        title = "SHOUTcast"
        version = 1.2
        homepage = "http://www.shoutcast.com/" 
        base_url = "http://shoutcast.com/"
        listformat = "audio/x-scpls"

        # settings
        config = [
        ]







<







34
35
36
37
38
39
40

41
42
43
44
45
46
47
# SHOUTcast data module                                          ----------------------------------------
class shoutcast(channels.ChannelPlugin):

        # desc
        api = "streamtuner2"
        module = "shoutcast"
        title = "SHOUTcast"

        homepage = "http://www.shoutcast.com/" 
        base_url = "http://shoutcast.com/"
        listformat = "audio/x-scpls"

        # settings
        config = [
        ]

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

1
2
3
4


5
6
7
8
9
10
11
12
13
14
15
#
# api: streamtuner2
# title: radio scheduler
# description: time play/record events for radio stations


# depends: kronos
# version: 0.5
# config:
# category: features
# priority: optional
# support: unsupported
#
# Okay, while programming this, I missed the broadcast I wanted to hear. Again(!)
# But still this is a useful extension, as it allows recording and playing specific
# stations at a programmed time and interval. It accepts a natural language time
# string when registering a stream. (Via streams menu > extension > add timer)


|
|
>
>


<
<







1
2
3
4
5
6
7
8


9
10
11
12
13
14
15
#
# api: streamtuner2
# title: Recording timer
# description: Schedules play/record events for bookmarked radio stations.
# type: feature
# category: ui
# depends: kronos
# version: 0.5


# priority: optional
# support: unsupported
#
# Okay, while programming this, I missed the broadcast I wanted to hear. Again(!)
# But still this is a useful extension, as it allows recording and playing specific
# stations at a programmed time and interval. It accepts a natural language time
# string when registering a stream. (Via streams menu > extension > add timer)
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# timed events (play/record) within bookmarks tab
class timer:

    # plugin info
    module = "timer"
    title = "Timer"
    version = 0.5
    
    
    # configuration settings
    config = [
    ]
    timefield = "playing"
    







<







31
32
33
34
35
36
37

38
39
40
41
42
43
44

# timed events (play/record) within bookmarks tab
class timer:

    # plugin info
    module = "timer"
    title = "Timer"

    
    
    # configuration settings
    config = [
    ]
    timefield = "playing"
    

Modified channels/xiph.py from [4dfeffc128] to [b7c1ca97af].

1
2
3
4


5

6
7
8
9
10
11
12
#
# api: streamtuner2
# title: Xiph.org
# description: Xiph/ICEcast radio directory


# version: 0.3

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



|
>
>

>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#
# api: streamtuner2
# title: Xiph.org
# description: ICEcast radio directory. Now utilizes a cached JSON API.
# type: channel
# category: radio
# version: 0.3
# 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# I wonder what that is for                                             ---------------------------------------
class xiph (ChannelPlugin):

        # desc
        api = "streamtuner2"
        module = "xiph"
        title = "Xiph.org"
        version = 0.3
        homepage = "http://dir.xiph.org/"
        #base_url = "http://api.dir.xiph.org/"
        json_url = "http://api.include-once.org/xiph/cache.php"
        listformat = "url/http"
        config = [
           {"name":"xiph_min_bitrate", "value":64, "type":"int", "description":"minimum bitrate, filter anything below", "category":"filter"}
        ]







<







38
39
40
41
42
43
44

45
46
47
48
49
50
51
# I wonder what that is for                                             ---------------------------------------
class xiph (ChannelPlugin):

        # desc
        api = "streamtuner2"
        module = "xiph"
        title = "Xiph.org"

        homepage = "http://dir.xiph.org/"
        #base_url = "http://api.dir.xiph.org/"
        json_url = "http://api.include-once.org/xiph/cache.php"
        listformat = "url/http"
        config = [
           {"name":"xiph_min_bitrate", "value":64, "type":"int", "description":"minimum bitrate, filter anything below", "category":"filter"}
        ]

Modified st2.py from [7776cafb92] to [6bfb3a77f9].

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







|







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
# 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
# 
#
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
    from processing import Process as Thread
except:
    from threading import Thread
    Thread.stop = lambda self: None

# add library path
sys.path.insert(0, "/usr/share/streamtuner2")   # pre-defined directory for modules
sys.path.insert(0, "/usr/share/streamtuner2/bundle")   # external libraries
sys.path.insert(0, ".")   # development module path

# gtk modules
from mygtk import pygtk, gtk, gobject, ui_file, mygtk, ver as GTK_VER

# custom modules
from config import conf   # initializes itself, so all conf.vars are available right away
from config import __print__, dbg
import ahttp
import action  # needs workaround... (action.main=main)

from channels import *
import favicon


__version__ = "2.1.0"









|










>







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
    from processing import Process as Thread
except:
    from threading import Thread
    Thread.stop = lambda self: None

# add library path
sys.path.insert(0, "/usr/share/streamtuner2")   # pre-defined directory for modules
sys.path.append(   "/usr/share/streamtuner2/bundle")   # external libraries
sys.path.insert(0, ".")   # development module path

# gtk modules
from mygtk import pygtk, gtk, gobject, ui_file, mygtk, ver as GTK_VER

# custom modules
from config import conf   # initializes itself, so all conf.vars are available right away
from config import __print__, dbg
import ahttp
import action  # needs workaround... (action.main=main)
import channels
from channels import *
import favicon


__version__ = "2.1.0"


438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
                mygtk.do(lambda:self.statusbar.push(sbar_cid, text))
            pass


        # load plugins from /usr/share/streamtuner2/channels/
        def load_plugin_channels(self):

            # find plugin files
            ls = os.listdir(conf.share + "/channels/")
            ls = [fn[:-3] for fn in ls if re.match("^[a-z][\w\d_]+\.py$", fn)]
            
            # resort with tab order
            order = [module.strip() for module in conf.channel_order.lower().replace(".","_").replace("-","_").split(",")]
            ls = [module for module in (order) if (module in ls)] + [module for module in (ls) if (module not in order)]

            # step through
            for module in ls:
                gui_startup(2/10.0 + 7/10.0 * float(ls.index(module))/len(ls), "loading module "+module)
                                
                # skip module if disabled
                if conf.plugins.get(module, 1) == False:







|
<
<
|
<
<
<







439
440
441
442
443
444
445
446


447



448
449
450
451
452
453
454
                mygtk.do(lambda:self.statusbar.push(sbar_cid, text))
            pass


        # load plugins from /usr/share/streamtuner2/channels/
        def load_plugin_channels(self):

            # find and order plugin files


            ls = channels.module_list()




            # step through
            for module in ls:
                gui_startup(2/10.0 + 7/10.0 * float(ls.index(module))/len(ls), "loading module "+module)
                                
                # skip module if disabled
                if conf.plugins.get(module, 1) == False:
749
750
751
752
753
754
755

756
757
758
759


760

761
762
763
764
765
766
767

# aux win: settings UI
class config_dialog (auxiliary_window):


        # display win_config, pre-fill text fields from global conf. object
        def open(self, widget):

            self.add_plugins()
            self.apply(conf.__dict__, "config_", 0)
            #self.win_config.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#443399'))
            self.combobox_theme()


            self.win_config.show()



        def hide(self, *args):
            self.win_config.hide()
            return True

        







>
|
<
<
|
>
>

>







745
746
747
748
749
750
751
752
753


754
755
756
757
758
759
760
761
762
763
764
765

# aux win: settings UI
class config_dialog (auxiliary_window):


        # display win_config, pre-fill text fields from global conf. object
        def open(self, widget):
            if self.first_open:
                self.add_plugins()


                self.combobox_theme()
                self.first_open = 0
            self.apply(conf.__dict__, "config_", 0)
            self.win_config.show()
        first_open = 1


        def hide(self, *args):
            self.win_config.hide()
            return True

        
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831

832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859






860
861
862
863



864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
        def apply_theme(self):
            if self.theme.get_active() >= 0:
                conf.theme = self.theme.get_model()[ self.theme.get_active()][0]
                main.load_theme()


        # add configuration setting definitions from plugins
        once = 0
        def add_plugins(self):
            if self.once:
                return

            for name,enabled in conf.plugins.items():

                # add plugin load entry
                if name:
                    label = ("enable ⎗ %s channel" if self.channels.get(name) else "use ⎗ %s plugin")
                    cb =  gtk.ToggleButton(label=label % name)

                    self.add_( "config_plugins_"+name, cb )#, label=None, color="#ddd" )

                # look up individual plugin options, if loaded
                if self.channels.get(name) or self.features.get(name):
                    c = self.channels.get(name) or self.features.get(name)
                    for opt in c.config:

                        # default values are already in conf[] dict (now done in conf.add_plugin_defaults)
                            
                        # display checkbox or text entry
                        if opt["type"] == "boolean":
                            cb = gtk.CheckButton(opt["description"])
                            #cb.set_line_wrap(True)
                            self.add_( "config_"+opt["name"], cb )
                        else:
                            self.add_( "config_"+opt["name"], gtk.Entry(), opt["description"] )

                # spacer 
                self.add_( "filler_pl_"+name, gtk.HSeparator() )
            self.once = 1


        # put gtk widgets into config dialog notebook
        def add_(self, id, w, label=None, color=""):
            w.set_property("visible", True)
            main.widgets[id] = w
            if label:
                w.set_width_chars(10)






                label = gtk.Label(label)
                label.set_property("visible", True)
                label.set_line_wrap(True) 
                label.set_size_request(250, -1)



                vbox = gtk.HBox(homogeneous=False, spacing=10)
                vbox.set_property("visible", True)
                vbox.pack_start(w, expand=False, fill=False)
                vbox.pack_start(label, expand=True, fill=True)
                w = vbox
            if color:
                w = mygtk.bg(w, color)
            self.plugin_options.pack_start(w)

        
        # save config
        def save(self, widget):
            self.apply(conf.__dict__, "config_", 1)
            self.apply_theme()
            conf.save(nice=1)







<

<
<

|



<
|
>
|


















<








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







812
813
814
815
816
817
818

819


820
821
822
823
824

825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845

846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871



872
873
874
875
876
877
878
        def apply_theme(self):
            if self.theme.get_active() >= 0:
                conf.theme = self.theme.get_model()[ self.theme.get_active()][0]
                main.load_theme()


        # add configuration setting definitions from plugins

        def add_plugins(self):



            for name,meta in channels.module_meta().items():

                # add plugin load entry
                if name:

                    cb = gtk.CheckButton(name)
                    cb.child.set_markup("<b>%s</b> <i>(%s)</i> %s\n<small>%s</small>" % (meta["title"], meta["type"], meta.get("version", ""), meta["description"]))
                    self.add_( "config_plugins_"+name, cb )

                # look up individual plugin options, if loaded
                if self.channels.get(name) or self.features.get(name):
                    c = self.channels.get(name) or self.features.get(name)
                    for opt in c.config:

                        # default values are already in conf[] dict (now done in conf.add_plugin_defaults)
                            
                        # display checkbox or text entry
                        if opt["type"] == "boolean":
                            cb = gtk.CheckButton(opt["description"])
                            #cb.set_line_wrap(True)
                            self.add_( "config_"+opt["name"], cb )
                        else:
                            self.add_( "config_"+opt["name"], gtk.Entry(), opt["description"] )

                # spacer 
                self.add_( "filler_pl_"+name, gtk.HSeparator() )



        # put gtk widgets into config dialog notebook
        def add_(self, id, w, label=None, color=""):
            w.set_property("visible", True)
            main.widgets[id] = w
            if label:
                w.set_width_chars(10)
                w = self.hbox(w, self.label(label))
            if color:
                w = mygtk.bg(w, color)
            self.plugin_options.pack_start(w)

        def label(self, label):
            label = gtk.Label(label)
            label.set_property("visible", True)
            label.set_line_wrap(True) 
            label.set_size_request(250, -1)
            return label

        def hbox(self, w1, w2):
            vbox = gtk.HBox(homogeneous=False, spacing=10)
            vbox.set_property("visible", True)
            vbox.pack_start(w1, expand=False, fill=False)
            vbox.pack_start(w2, expand=True, fill=True)
            return vbox




        
        # save config
        def save(self, widget):
            self.apply(conf.__dict__, "config_", 1)
            self.apply_theme()
            conf.save(nice=1)