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

⌈⌋ ⎇ branch:  streamtuner2


Diff

Differences From Artifact [a25ff9d0d3]:

To Artifact [1a108a0e92]:


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
#!/usr/bin/env python
#
# encoding: UTF-8
# api: python
# type: application
# title: streamtuner2
# description: Directory browser for internet radio, audio and video streams
# version: 2.1.5

# author: Mario Salzer <mario@include-once.org>
# license: Public Domain
# url: http://freshcode.club/projects/streamtuner2
# config:  
#   { type: env, name: http_proxy, description: proxy for HTTP access }
#   { type: env, name: XDG_CONFIG_HOME, description: relocates user .config subdirectory }
# category: sound
# depends: pygtk | gi, threading, requests, pyquery, lxml
# id: streamtuner2
# pack: *.py, gtk*.xml, st2.py=/usr/bin/streamtuner2, channels/__init__.py, bundle/*.py,
#   streamtuner2.desktop=/usr/share/applications/, README=/usr/share/doc/streamtuner2/,
#   NEWS.gz=/usr/share/doc/streamtuner2/changelog.gz, help/streamtuner2.1=/usr/share/man/man1/,
#   help/*page=/usr/share/doc/streamtuner2/help/, help/img/*=/usr/share/doc/streamtuner2/help/img/,
#   streamtuner2.png, logo.png=/usr/share/pixmaps/streamtuner2.png,
# architecture: all
#
# Streamtuner2 is a GUI for browsing internet radio directories, music
# collections, and video services - grouped by genres or categories.
# It utilizes installed audio players, and streamripper for recording.
#
# It's an independent rewrite of streamtuner1. Being written in Python,
# can be more easily extended and fixed. The mix of JSON APIs, regex
# or PyQuery extraction makes list generation simpler and more robust.
#
# Primarily radio stations are displayed, some channels however are music
# collections. Commercial and sign-up services are not an objective.








>


















|







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
#!/usr/bin/env python
#
# encoding: UTF-8
# api: python
# type: application
# title: streamtuner2
# description: Directory browser for internet radio, audio and video streams
# version: 2.1.5
# state: beta
# author: Mario Salzer <mario@include-once.org>
# license: Public Domain
# url: http://freshcode.club/projects/streamtuner2
# config:  
#   { type: env, name: http_proxy, description: proxy for HTTP access }
#   { type: env, name: XDG_CONFIG_HOME, description: relocates user .config subdirectory }
# category: sound
# depends: pygtk | gi, threading, requests, pyquery, lxml
# id: streamtuner2
# pack: *.py, gtk*.xml, st2.py=/usr/bin/streamtuner2, channels/__init__.py, bundle/*.py,
#   streamtuner2.desktop=/usr/share/applications/, README=/usr/share/doc/streamtuner2/,
#   NEWS.gz=/usr/share/doc/streamtuner2/changelog.gz, help/streamtuner2.1=/usr/share/man/man1/,
#   help/*page=/usr/share/doc/streamtuner2/help/, help/img/*=/usr/share/doc/streamtuner2/help/img/,
#   streamtuner2.png, logo.png=/usr/share/pixmaps/streamtuner2.png,
# architecture: all
#
# Streamtuner2 is a GUI for browsing internet radio directories, music
# collections, and video services - grouped by genres or categories.
# It runs your preferred audio player, and streamripper for recording.
#
# It's an independent rewrite of streamtuner1. Being written in Python,
# can be more easily extended and fixed. The mix of JSON APIs, regex
# or PyQuery extraction makes list generation simpler and more robust.
#
# Primarily radio stations are displayed, some channels however are music
# collections. Commercial and sign-up services are not an objective.
46
47
48
49
50
51
52
53
54
55
56
57
58

59
60
61
62
63
64
65
from threading import Thread

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

# gtk modules
from uikit import pygtk, gtk, gobject, ui_file, uikit, gui_startup, AboutStreamtuner2

# custom modules
from config import *   # initializes itself, so all conf.vars are available right away
import ahttp
import action

import favicon
import channels
import channels.bookmarks
import channels.configwin
import channels.streamedit
import channels.search








|





>







47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from threading import Thread

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

# gtk modules
from uikit import pygtk, gtk, gobject, uikit, ui_xml, gui_startup, AboutStreamtuner2

# custom modules
from config import *   # initializes itself, so all conf.vars are available right away
import ahttp
import action
import logo
import favicon
import channels
import channels.bookmarks
import channels.configwin
import channels.streamedit
import channels.search

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
    channel_names = ["bookmarks"]    # order of channel notebook tabs
    current_channel = "bookmarks"    # currently selected channel name (as index in self.channels{})


    # constructor
    def __init__(self):
        
        # gtkrc stylesheet
        self.load_theme(), gui_startup(1/20.0)

        # instantiate gtk/glade widgets in current object
        gtk.Builder.__init__(self)
        gtk.Builder.add_from_file(self, conf.find_in_dirs([".", conf.share], ui_file)), gui_startup(2/20.0)
        # manual gtk operations
        self.extensionsCTM.set_submenu(self.extensions)  # duplicates Station>Extension menu into stream context menu


        # initialize built-in plugins
        self.channels = {
          "bookmarks": channels.bookmarks.bookmarks(parent=self),   # this the remaining built-in channel
        }
        # dialogs that are connected to main
        self.features = {
          "search": channels.search.search(self),
          "configwin": channels.configwin.configwin(self),
          "streamedit": channels.streamedit.streamedit(self),
        }
        gui_startup(3/20.0)
        # append other channel modules and plugins
        self.load_plugin_channels()


        # load application state (widget sizes, selections, etc.)
        try:
            winlayout = conf.load("window")







|
|
<
<
|
|
<
|
>











|







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
    channel_names = ["bookmarks"]    # order of channel notebook tabs
    current_channel = "bookmarks"    # currently selected channel name (as index in self.channels{})


    # constructor
    def __init__(self):
        
        # Load stylesheet, instantiate GtkBuilder in self, menu and logo hooks
        gui_startup(0/20.0), self.load_theme()


        gui_startup(1/20.0), gtk.Builder.__init__(self)
        gui_startup(1/20.0), gtk.Builder.add_from_string(self, ui_xml)

        gui_startup(3/20.0), self.extensionsCTM.set_submenu(self.extensions)  # duplicates Station>Extension menu into stream context menu
        self.img_logo.set_from_pixbuf(uikit.pixbuf(logo.png))

        # initialize built-in plugins
        self.channels = {
          "bookmarks": channels.bookmarks.bookmarks(parent=self),   # this the remaining built-in channel
        }
        # dialogs that are connected to main
        self.features = {
          "search": channels.search.search(self),
          "configwin": channels.configwin.configwin(self),
          "streamedit": channels.streamedit.streamedit(self),
        }
        gui_startup(4/20.0)
        # append other channel modules and plugins
        self.load_plugin_channels()


        # load application state (widget sizes, selections, etc.)
        try:
            winlayout = conf.load("window")
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
        gui_startup(18/20.0)
        self.current_channel = self.current_channel_gtk()
        try: self.channel().first_show()
        except: __print__(dbg.INIT, "main.__init__: current_channel.first_show() initialization error")

  
        # bind gtk/glade event names to functions
        gui_startup(19/20.0)
        self.connect_signals(dict({
            "gtk_main_quit" : self.gtk_main_quit,                # close window
            # treeviews / notebook
            "on_stream_row_activated" : self.on_play_clicked,    # double click in a streams list
            "on_category_clicked": self.on_category_clicked,     # new selection in category list
            "on_notebook_channels_switch_page": self.channel_switch,   # channel notebook tab changed
            "station_context_menu": lambda tv,ev: self.station_context_menu(tv,ev),







|







138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
        gui_startup(18/20.0)
        self.current_channel = self.current_channel_gtk()
        try: self.channel().first_show()
        except: __print__(dbg.INIT, "main.__init__: current_channel.first_show() initialization error")

  
        # bind gtk/glade event names to functions
        gui_startup(19.75/20.0)
        self.connect_signals(dict({
            "gtk_main_quit" : self.gtk_main_quit,                # close window
            # treeviews / notebook
            "on_stream_row_activated" : self.on_play_clicked,    # double click in a streams list
            "on_category_clicked": self.on_category_clicked,     # new selection in category list
            "on_notebook_channels_switch_page": self.channel_switch,   # channel notebook tab changed
            "station_context_menu": lambda tv,ev: self.station_context_menu(tv,ev),
202
203
204
205
206
207
208
209
210

211
212
213
214
215
216
217
            "streamedit_open": self.streamedit.open,
            "streamedit_save": self.streamedit.save,
            "streamedit_new": self.streamedit.new,
            "streamedit_cancel": self.streamedit.cancel,
        }, **self.add_signals))
        
        # actually display main window
        gui_startup(98.9/100.0)
        self.win_streamtuner2.show()

        

    #-- Shortcut for glade.get_widget()
    # Allows access to widgets as direct attributes instead of using .get_widget()
    # Also looks in self.channels[] for the named channel plugins
    def __getattr__(self, name):
        if (name in self.channels):







<

>







202
203
204
205
206
207
208

209
210
211
212
213
214
215
216
217
            "streamedit_open": self.streamedit.open,
            "streamedit_save": self.streamedit.save,
            "streamedit_new": self.streamedit.new,
            "streamedit_cancel": self.streamedit.cancel,
        }, **self.add_signals))
        
        # actually display main window

        self.win_streamtuner2.show()
        gui_startup(1.0)
        

    #-- Shortcut for glade.get_widget()
    # Allows access to widgets as direct attributes instead of using .get_widget()
    # Also looks in self.channels[] for the named channel plugins
    def __getattr__(self, name):
        if (name in self.channels):
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416

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

        # initialize plugin modules (pre-ordered)
        ls = channels.module_list()
        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:
                __print__(dbg.STAT, "disabled plugin:", module)
                continue
            # or if it's a built-in (already imported)
            elif module in self.features or module in self.channels:







|







402
403
404
405
406
407
408
409
410
411
412
413
414
415
416

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

        # initialize plugin modules (pre-ordered)
        ls = channels.module_list()
        for module in ls:
            gui_startup(4/20.0 + 13.5/20.0 * float(ls.index(module))/len(ls), "loading module "+module)
                            
            # skip module if disabled
            if conf.plugins.get(module, 1) == False:
                __print__(dbg.STAT, "disabled plugin:", module)
                continue
            # or if it's a built-in (already imported)
            elif module in self.features or module in self.channels:
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536




#-- run main
if __name__ == "__main__":


    # graphical
    if len(sys.argv) < 2 or "--gtk3" in sys.argv:

        # prepare for threading in Gtk+ callbacks
        gobject.threads_init()
        gui_startup(1/100.0)

        # prepare main window
        main = StreamTunerTwo()

        # module coupling
        action.main = main      # action (play/record) module needs a reference to main window for gtk interaction and some URL/URI callbacks
        action = action.action  # shorter name
        ahttp.feedback = main.status  # http module gives status feedbacks too

        # first invocation
        if (conf.get("firstrun")):
            main.configwin.open(None)
            del conf.firstrun

        # run
        gui_startup(100/100.0)
        gtk.main()
        __print__(dbg.PROC, r" gtk_main_quit ")
        
    # invoke command-line interface
    else:
        import cli
        cli.StreamTunerCLI()









<





<















<









498
499
500
501
502
503
504

505
506
507
508
509

510
511
512
513
514
515
516
517
518
519
520
521
522
523
524

525
526
527
528
529
530
531
532
533




#-- run main
if __name__ == "__main__":


    # graphical
    if len(sys.argv) < 2 or "--gtk3" in sys.argv:

        # prepare for threading in Gtk+ callbacks
        gobject.threads_init()


        # prepare main window
        main = StreamTunerTwo()

        # module coupling
        action.main = main      # action (play/record) module needs a reference to main window for gtk interaction and some URL/URI callbacks
        action = action.action  # shorter name
        ahttp.feedback = main.status  # http module gives status feedbacks too

        # first invocation
        if (conf.get("firstrun")):
            main.configwin.open(None)
            del conf.firstrun

        # run

        gtk.main()
        __print__(dbg.PROC, r" gtk_main_quit ")
        
    # invoke command-line interface
    else:
        import cli
        cli.StreamTunerCLI()