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: gtk, pygtk, xml.dom.minidom, threading, lxml, pyquery, kronos
# version: 2.0.9.5
# 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.0.9.7
# 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
#
#
|
︙ | | | ︙ | |
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
|
#
#
""" project status """
#
# The application runs mostly stable. The GUI interfaces are workable.
# There haven't been any optimizations regarding memory usage and
# performance. The current internal API is acceptable. Documentation is
# coming up.
#
# current bugs:
# - audio- and list-format support is not very robust / needs better API
# - lots of GtkWarning messages
# - not all keyboard shortcuts work
# - in-list search doesn't work in our treeviews (???)
# - JSON files are only trouble: loading of data files might lead to more
# errors now, even if pson module still falls back on old method
# (unicode strings from json.load are useless to us, require typecasts)
# (nonsupport of tuples led to regression in mygtk.app_restore)
# (sometimes we receive 8bit-content, which the json module can't save)
#
# features:
# - treeview lists are created from datamap[] structure and stream{} dicts
# - channel categories are built-in defaults (can be freshened up however)
# - config vars and cache data get stored as JSON in ~/.config/streamtuner2/
#
# missing:
|
>
|
<
<
<
<
<
<
<
<
|
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
#
#
""" project status """
#
# The application runs mostly stable. The GUI interfaces are workable.
# It's supposed to run on Gtk2 and Gtk3. Python3 support is still WIP.
# There haven't been any optimizations regarding memory usage and
# performance. The current internal API is vastly undocumented.
#
# current bugs:
# - audio- and list-format support is not very robust / needs better API
# - not all keyboard shortcuts work
#
# features:
# - treeview lists are created from datamap[] structure and stream{} dicts
# - channel categories are built-in defaults (can be freshened up however)
# - config vars and cache data get stored as JSON in ~/.config/streamtuner2/
#
# missing:
|
︙ | | | ︙ | |
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
111
112
113
114
|
# to read the config data -> limited risk, since it's only local files
# - HIGH RISK: no verification of downloaded favicon image files (ico/png),
# as they are passed to gtk.gdk.Pixbuf (OTOH data pre-filtered by Google)
# - MEDIUM: audio players / decoders are easily affected by buffer overflows
# from corrupt mp3/stream data, and streamtuner2 executes them
# - but since that's the purpose -> no workaround
#
# still help wanted on:
# - any of the above
# - new plugins (local file viewer)
# - nicer logo (or donations accepted to consult graphics designer)
#
# standard modules
import sys
import os, os.path
import re
import copy
import urllib
# threading or processing module
try:
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, ".") # pre-defined directory for modules
# gtk modules
from mygtk import pygtk, gtk, gobject, ui_file, mygtk
# custom modules
from config import conf # initializes itself, so all conf.vars are available right away
from config import __print__, dbg
import http
import action # needs workaround... (action.main=main)
from channels import *
import favicon
#from pq import pq
# this represents the main window
# and also contains most application behaviour
main = None
class StreamTunerTwo(gtk.Builder):
|
<
<
<
<
<
<
<
>
|
|
|
<
|
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
|
# to read the config data -> limited risk, since it's only local files
# - HIGH RISK: no verification of downloaded favicon image files (ico/png),
# as they are passed to gtk.gdk.Pixbuf (OTOH data pre-filtered by Google)
# - MEDIUM: audio players / decoders are easily affected by buffer overflows
# from corrupt mp3/stream data, and streamtuner2 executes them
# - but since that's the purpose -> no workaround
#
# standard modules
import sys
import os, os.path
import re
# threading or processing module
try:
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
# this represents the main window
# and also contains most application behaviour
main = None
class StreamTunerTwo(gtk.Builder):
|
︙ | | | ︙ | |
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
|
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: station_context_menu(tv,ev),
# toolbar
|
|
|
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
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( list({
"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: station_context_menu(tv,ev),
# toolbar
|
︙ | | | ︙ | |
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
|
"search_google": search.google,
"search_cancel": search.cancel,
"true": lambda w,*args: True,
"streamedit_open": streamedit.open,
"streamedit_save": streamedit.save,
"streamedit_new": streamedit.new,
"streamedit_cancel": streamedit.cancel,
}.items() + self.add_signals.items() ))
# actually display main window
gui_startup(99/100.0)
self.win_streamtuner2.show()
# WHY DON'T YOU WANT TO WORK?!
#self.shoutcast.gtk_list.set_enable_search(True)
#self.shoutcast.gtk_list.set_search_column(4)
#-- 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 (self.channels.has_key(name)):
return self.channels[name] # like self.shoutcast
else:
return self.get_object(name) # or gives an error if neither exists
# custom-named widgets are available from .widgets{} not via .get_widget()
def get_widget(self, name):
if self.widgets.has_key(name):
return self.widgets[name]
else:
return gtk.Builder.get_object(self, name)
|
|
|
|
|
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
|
"search_google": search.google,
"search_cancel": search.cancel,
"true": lambda w,*args: True,
"streamedit_open": streamedit.open,
"streamedit_save": streamedit.save,
"streamedit_new": streamedit.new,
"streamedit_cancel": streamedit.cancel,
}.items() ) + list( self.add_signals.items() ) ))
# actually display main window
gui_startup(99/100.0)
self.win_streamtuner2.show()
# WHY DON'T YOU WANT TO WORK?!
#self.shoutcast.gtk_list.set_enable_search(True)
#self.shoutcast.gtk_list.set_search_column(4)
#-- 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):
return self.channels[name] # like self.shoutcast
else:
return self.get_object(name) # or gives an error if neither exists
# custom-named widgets are available from .widgets{} not via .get_widget()
def get_widget(self, name):
if name in self.widgets:
return self.widgets[name]
else:
return gtk.Builder.get_object(self, name)
|
︙ | | | ︙ | |
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
|
gtk.rc_parse(f)
pass
# end application and gtk+ main loop
def gtk_main_quit(self, widget, *x):
if conf.auto_save_appstate:
self.app_state(widget)
gtk.main_quit()
|
>
|
>
>
|
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
|
gtk.rc_parse(f)
pass
# end application and gtk+ main loop
def gtk_main_quit(self, widget, *x):
if conf.auto_save_appstate:
try: # doesn't work with gtk3 yet (probably just hooking at the wrong time)
self.app_state(widget)
except:
None
gtk.main_quit()
|
︙ | | | ︙ | |
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
|
# right click in streams/stations TreeView
def station_context_menu(treeview, event):
# right-click ?
if event.button >= 3:
path = treeview.get_path_at_pos(int(event.x), int(event.y))[0]
treeview.grab_focus()
treeview.set_cursor(path, None, False)
main.streamactions.popup(None, None, None, event.button, event.time)
return None
# we need to pass on to normal left-button signal handler
else:
return False
# this works better as callback function than as class - because of False/Object result for event trigger
# encapsulates references to gtk objects AND properties in main window
class auxiliary_window(object):
def __getattr__(self, name):
if main.__dict__.has_key(name):
return main.__dict__[name]
elif StreamTunerTwo.__dict__.has_key(name):
return StreamTunerTwo.__dict__[name]
else:
return main.get_widget(name)
""" allows to use self. and main. almost interchangably """
|
|
>
>
>
>
|
|
|
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
|
# right click in streams/stations TreeView
def station_context_menu(treeview, event):
# right-click ?
if event.button >= 3:
path = treeview.get_path_at_pos(int(event.x), int(event.y))[0]
treeview.grab_focus()
treeview.set_cursor(path, None, False)
main.streamactions.popup(
parent_menu_shell=None, parent_menu_item=None, func=None,
button=event.button, activate_time=event.time,
data=None
)
return None
# we need to pass on to normal left-button signal handler
else:
return False
# this works better as callback function than as class - because of False/Object result for event trigger
# encapsulates references to gtk objects AND properties in main window
class auxiliary_window(object):
def __getattr__(self, name):
if name in main.__dict__:
return main.__dict__[name]
elif name in StreamTunerTwo.__dict__:
return StreamTunerTwo.__dict__[name]
else:
return main.get_widget(name)
""" allows to use self. and main. almost interchangably """
|
︙ | | | ︙ | |
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
|
def hide(self, *args):
self.win_config.hide()
return True
# set/load values between gtk window and conf. dict
def apply(self, config, prefix="config_", save=0):
for key,val in config.iteritems():
# map non-alphanumeric chars from config{} to underscores in according gtk widget names
id = re.sub("[^\w]", "_", key)
w = main.get_widget(prefix + id)
__print__(dbg.CONF, "config", ("save" if save else "load"), prefix+id, w, val)
# recurse into dictionaries, transform: conf.play.audio/mp3 => conf.play_audio_mp3
if (type(val) == dict):
self.apply(val, prefix + id + "_", save)
|
|
|
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
|
def hide(self, *args):
self.win_config.hide()
return True
# set/load values between gtk window and conf. dict
def apply(self, config, prefix="config_", save=0):
for key,val in config.items():
# map non-alphanumeric chars from config{} to underscores in according gtk widget names
id = re.sub("[^\w]", "_", key)
w = main.get_widget(prefix + id)
__print__(dbg.CONF, "config", ("save" if save else "load"), prefix+id, w, val)
# recurse into dictionaries, transform: conf.play.audio/mp3 => conf.play_audio_mp3
if (type(val) == dict):
self.apply(val, prefix + id + "_", save)
|
︙ | | | ︙ | |
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
|
# add configuration setting definitions from plugins
once = 0
def add_plugins(self):
if self.once:
return
for name,enabled in conf.plugins.iteritems():
# 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" )
|
|
|
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
|
# 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" )
|
︙ | | | ︙ | |
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
|
# First we'll generate a list of current bookmark stream urls, and then
# remove all but those from the currently UPDATED_channel + category.
# This step is most likely redundant, but prevents accidently re-rewriting
# stations that are in two channels (=duplicates with different PLS urls).
check = {"http//": "[row]"}
check = dict((row["url"],row) for row in fav)
# walk through all channels/streams
for chname,channel in main.channels.iteritems():
for cat,streams in channel.streams.iteritems():
# keep the potentially changed rows
if (chname == updated_channel) and (cat == updated_category):
freshened_streams = streams
# remove unchanged urls/rows
else:
|
|
|
|
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
|
# First we'll generate a list of current bookmark stream urls, and then
# remove all but those from the currently UPDATED_channel + category.
# This step is most likely redundant, but prevents accidently re-rewriting
# stations that are in two channels (=duplicates with different PLS urls).
check = {"http//": "[row]"}
check = dict((row["url"],row) for row in fav)
# walk through all channels/streams
for chname,channel in main.channels.items():
for cat,streams in channel.streams.items():
# keep the potentially changed rows
if (chname == updated_channel) and (cat == updated_category):
freshened_streams = streams
# remove unchanged urls/rows
else:
|
︙ | | | ︙ | |
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
|
#-- run main ---------------------------------------------
if __name__ == "__main__":
#-- global configuration settings
"conf = Config()" # already happened with "from config import conf"
# graphical
if len(sys.argv) < 2:
# 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
http.feedback = main.status # http module gives status feedbacks too
# first invocation
if (conf.get("firstrun")):
config_dialog.open(None)
del conf.firstrun
|
|
|
|
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
|
#-- run main ---------------------------------------------
if __name__ == "__main__":
#-- global configuration settings
"conf = Config()" # already happened with "from config import conf"
# 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")):
config_dialog.open(None)
del conf.firstrun
|
︙ | | | ︙ | |