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
|
# encoding: UTF-8
# api: streamtuner2
# type: base
# category: ui
# title: Channel plugins
# description: Base implementation for channels and feature plugins
# version: 1.2
# license: public domain
# author: mario
# url: http://fossil.include-once.org/streamtuner2/
# pack:
# bookmarks.py configwin.py streamedit.py history.py search.py links.py
# icast.py internet_radio.py itunes.py jamendo.py live365.py global_key.py
# modarchive.py myoggradio.py punkcast.py radiobrowser.py radiotray.py
# shoutcast.py surfmusik.py timer.py tunein.py xiph.py youtube.py
# exportcat.py useragentswitcher.py
# config: -
# priority: core
#
#
# Just exports GenericChannel and ChannelPlugin.
# 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
# channels don't have a pre-defined Notebook tab in the glade
# file. They derive from the ChannelPlugins class instead, which
# adds the required gtk Widgets manually.
#
# Makes module scanning available. Checks for conf.share, so
# should pick up /usr/share/streamtuner2/channels/*.py plugins
# as well as local ./channels/*.* - Needs rework for in-zip
# searching.
#
import gtk
from uikit import uikit
from config import *
import ahttp as http
import action
import favicon
|
|
|
<
<
|
|
>
<
|
|
|
|
<
<
<
<
<
|
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
|
# encoding: UTF-8
# api: streamtuner2
# type: class
# category: ui
# title: Channel plugins
# description: Base implementation for channels and feature plugins
# version: 1.3
# license: public domain
# author: mario
# url: http://fossil.include-once.org/streamtuner2/
# pack:
# bookmarks.py configwin.py streamedit.py history.py search.py links.py
# icast.py internet_radio.py itunes.py jamendo.py live365.py global_key.py
# modarchive.py myoggradio.py punkcast.py radiobrowser.py radiotray.py
# shoutcast.py surfmusik.py timer.py tunein.py xiph.py youtube.py
# exportcat.py useragentswitcher.py
# config: -
# priority: core
#
# GenericChannel implements the basic GUI functions and defines
# the default channel data structure. It implements fallback logic
# for all other channel implementations. Only `bookmarks` uses it
# directly.
#
# All other plugins don't have a pre-defined Notebook tab in the
# GtkBuilder description. They derive from ChannelPlugins therefore,
# which constructs and registers the required gtk widgets manually.
import gtk
from uikit import uikit
from config import *
import ahttp as http
import action
import favicon
|
︙ | | | ︙ | |
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
|
# generic channel module ---------------------------------------
class GenericChannel(object):
# desc
meta = { "config": [] }
homepage = "http://fossil.include-once.org/streamtuner2/"
base_url = ""
listformat = "pls"
audioformat = "audio/mpeg" # fallback value
config = []
has_search = False
# categories
categories = ["empty", ]
catmap = {}
current = ""
default = "empty"
shown = None # last selected entry in stream list, also indicator if notebook tab has been selected once / stream list of current category been displayed yet
# gui + data
streams = {} #meta information dicts
liststore = {} #gtk data structure
gtk_list = None #gtk widget
gtk_cat = None #gtk widget
# mapping of stream{} data into gtk treeview/treestore representation
datamap = [
# coltitle width [ datasrc key, type, renderer, attrs ] [cellrenderer2], ...
["", 20, ["state", str, "pixbuf", {}], ],
["Genre", 65, ['genre', str, "t", {}], ],
["Station Title",275,["title", str, "text", {"strikethrough":11, "cell-background":12, "cell-background-set":13}], ["favicon", gtk.gdk.Pixbuf, "pixbuf", {}], ],
|
>
<
|
<
|
<
|
|
|
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
|
# generic channel module ---------------------------------------
class GenericChannel(object):
# desc
meta = { "config": [] }
config = []
homepage = "http://fossil.include-once.org/streamtuner2/"
base_url = ""
listformat = "pls"
audioformat = "audio/mpeg" # fallback value
has_search = False
# categories
categories = ["empty", ]
catmap = {}
current = None
shown = None # last selected entry in stream list, also indicator if notebook tab has been selected once / stream list of current category been displayed yet
# gui + data
streams = {} # Station list dict, associates each genre to a list of stream rows
gtk_list = None # Gtk widget for station treeview
gtk_cat = None # Gtk widget for category columns
# mapping of stream{} data into gtk treeview/treestore representation
datamap = [
# coltitle width [ datasrc key, type, renderer, attrs ] [cellrenderer2], ...
["", 20, ["state", str, "pixbuf", {}], ],
["Genre", 65, ['genre', str, "t", {}], ],
["Station Title",275,["title", str, "text", {"strikethrough":11, "cell-background":12, "cell-background-set":13}], ["favicon", gtk.gdk.Pixbuf, "pixbuf", {}], ],
|
︙ | | | ︙ | |
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
]
rowmap = [] # [state,genre,title,...] field enumeration still needed separately
titles = {} # for easier adapting of column titles in datamap
# for empty grouping / categories
placeholder = [dict(genre="./.", title="Subcategory placeholder", playing="./.", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-folder")]
empty_stub = [dict(genre="./.", title="No categories found (HTTP error)", playing="Try Channel→Reload Categories later..", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-stop")]
# regex
rx_www_url = re.compile("""(www(\.\w+[\w-]+){2,}|(\w+[\w-]+[ ]?\.)+(com|FM|net|org|de|PL|fr|uk))""", re.I)
#--------------------------- initialization --------------------------------
|
>
|
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
]
rowmap = [] # [state,genre,title,...] field enumeration still needed separately
titles = {} # for easier adapting of column titles in datamap
# for empty grouping / categories
placeholder = [dict(genre="./.", title="Subcategory placeholder", playing="./.", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-folder")]
empty_stub = [dict(genre="./.", title="No categories found (HTTP error)", playing="Try Channel→Reload Categories later..", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-stop")]
nothing_found = [dict(genre="./.", title="No contents found on directory server", playing="Notice", listeners=0, bitrate=0, state="gtk-info")]
# regex
rx_www_url = re.compile("""(www(\.\w+[\w-]+){2,}|(\w+[\w-]+[ ]?\.)+(com|FM|net|org|de|PL|fr|uk))""", re.I)
#--------------------------- initialization --------------------------------
|
︙ | | | ︙ | |
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
169
170
171
172
173
174
175
|
self.meta = plugin_meta(src = inspect.getcomments(inspect.getmodule(self)))
self.config = self.meta.get("config", [])
self.title = self.meta.get("title", self.module)
# add default options values to config.conf.* dict
conf.add_plugin_defaults(self.meta, self.module)
# stub for ST2 main window / dispatcher
self.parent = stub_parent(None)
# only if streamtuner2 is run in graphical mode
if (parent):
self.cache()
self.gui(parent)
pass
# initialize Gtk widgets / data objects
def gui(self, parent):
#print(self.module + ".gui()")
# save reference to main window/glade API
self.parent = parent
self.gtk_list = parent.get_widget(self.module+"_list")
self.gtk_cat = parent.get_widget(self.module+"_cat")
# category tree
self.display_categories()
#uikit.tree(self.gtk_cat, self.categories, title="Category", icon=gtk.STOCK_OPEN);
# update column names
for field,title in list(self.titles.items()):
self.update_datamap(field, title=title)
# prepare stream list
if (not self.rowmap):
for row in self.datamap:
for x in range(2, len(row)):
self.rowmap.append(row[x][0])
# load default category
#if (self.current):
# self.load(self.current)
#else:
if True:
uikit.columns(self.gtk_list, self.datamap, [])
# add to main menu
uikit.add_menu([parent.channelmenuitems], self.meta["title"], lambda w: parent.channel_switch_by_name(self.module) or 1)
# Statusbar stub (defers to parent/main window, if in GUI mode)
def status(self, *v):
if self.parent: self.parent.status(*v)
|
<
<
<
|
|
>
>
>
<
<
<
<
<
<
|
|
|
|
114
115
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
|
self.meta = plugin_meta(src = inspect.getcomments(inspect.getmodule(self)))
self.config = self.meta.get("config", [])
self.title = self.meta.get("title", self.module)
# add default options values to config.conf.* dict
conf.add_plugin_defaults(self.meta, self.module)
# Only if streamtuner2 is run in graphical mode
if (parent):
self.cache()
self.gui(parent)
# Stub for ST2 main window / dispatcher
else:
self.parent = stub_parent(None)
# initialize Gtk widgets / data objects
def gui(self, parent):
# save reference to main window/glade API
self.parent = parent
self.gtk_list = parent.get_widget(self.module+"_list")
self.gtk_cat = parent.get_widget(self.module+"_cat")
# category tree
self.display_categories()
# update column names
for field,title in list(self.titles.items()):
self.update_datamap(field, title=title)
# prepare stream list
if (not self.rowmap):
for row in self.datamap:
for x in range(2, len(row)):
self.rowmap.append(row[x][0])
# Initialize stations TreeView
uikit.columns(self.gtk_list, self.datamap, [])
# add to main menu
uikit.add_menu([parent.channelmenuitems], self.meta["title"], lambda w: parent.channel_switch_by_name(self.module) or 1)
# Statusbar stub (defers to parent/main window, if in GUI mode)
def status(self, *v):
if self.parent: self.parent.status(*v)
|
︙ | | | ︙ | |
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
|
# load data,
# update treeview content
def load(self, category, force=False):
# get data from cache or download
if (force or not category in self.streams):
__print__(dbg.PROC, "load", "update_streams")
self.parent.status("Updating streams...")
self.parent.status(-0.1)
if category == "empty":
new_streams = self.empty_stub
else:
new_streams = self.update_streams(category)
if new_streams:
|
|
|
|
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
|
# load data,
# update treeview content
def load(self, category, force=False):
# get data from cache or download
if (force or not category in self.streams):
__print__(dbg.PROC, "load", "update_streams")
self.status("Updating streams...")
self.status(-0.1)
if category == "empty":
new_streams = self.empty_stub
else:
new_streams = self.update_streams(category)
if new_streams:
|
︙ | | | ︙ | |
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
|
# invalidate gtk list cache
#if (self.liststore.has_key(category)):
# del self.liststore[category]
else:
# parse error
self.parent.status("category parsed empty.")
self.streams[category] = [{"title":"no contents found on directory server","bitrate":0,"max":0,"listeners":0,"playing":"error","favourite":0,"deleted":0}]
__print__(dbg.INFO, "Oooops, parser returned nothing for category " + category)
# assign to treeview model
#self.streams[self.default] = []
#if (self.liststore.has_key(category)): # was already loded before
# self.gtk_list.set_model(self.liststore[category])
#else: # currently list is new, had not been converted to gtk array before
# self.liststore[category] = \
uikit.do(lambda:uikit.columns(self.gtk_list, self.datamap, self.prepare(self.streams[category])))
# set pointer
self.current = category
self.parent.status("")
self.parent.status(1.0)
pass
# store current streams data
def save(self):
conf.save("cache/" + self.module, self.streams, gz=1)
|
|
|
<
|
|
|
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
|
# invalidate gtk list cache
#if (self.liststore.has_key(category)):
# del self.liststore[category]
else:
# parse error
self.status("Category parsed empty.")
self.streams[category] = self.nothing_found
__print__(dbg.INFO, "Oooops, parser returned nothing for category " + category)
# assign to treeview model
#if (self.liststore.has_key(category)): # was already loded before
# self.gtk_list.set_model(self.liststore[category])
#else: # currently list is new, had not been converted to gtk array before
# self.liststore[category] = \
uikit.do(lambda:uikit.columns(self.gtk_list, self.datamap, self.prepare(self.streams[category])))
# set pointer
self.current = category
self.status("")
self.status(1.0)
pass
# store current streams data
def save(self):
conf.save("cache/" + self.module, self.streams, gz=1)
|
︙ | | | ︙ | |
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
|
if self.current == category:
self.reload()
# display .current category, once notebook/channel tab is first opened
def first_show(self):
if (self.shown != 55555):
__print__(dbg.PROC, self.module+".first_show()")
# if category tree is empty, initialize it
if not self.categories:
__print__(dbg.PROC, self.module+"first_show: reload_categories");
#self.parent.thread(self.reload_categories)
try:
self.reload_categories()
except:
__print__(dbg.ERR, "HTTP error or extraction failure.")
self.categories = ["empty"]
self.display_categories()
self.current = self.categories.keys()[0]
__print__(dbg.STAT, "Use first category as current =", self.current)
self.load(self.current)
# load current category
else:
__print__(dbg.STAT, self.module+".first_show(): load current category =", self.current);
self.load(self.current)
# put selection/cursor on last position
try:
__print__(dbg.STAT, self.module+".first_show()", "select last known category treelist position =", self.shown)
self.gtk_list.get_selection().select_path(self.shown)
except:
pass
# this method will only be invoked once
self.shown = 55555
# update categories, save, and display
def reload_categories(self):
# get data and save
self.update_categories()
|
>
|
>
|
|
|
|
|
<
|
|
|
|
|
|
<
<
<
|
|
<
>
|
>
|
>
>
|
|
<
|
>
|
|
|
|
|
|
>
>
>
>
>
>
>
>
>
>
|
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
|
if self.current == category:
self.reload()
# display .current category, once notebook/channel tab is first opened
def first_show(self):
# Already processed
if (self.shown == 55555):
return
__print__(dbg.PROC, self.module, "→ first_show()", ", current=", self.current, ", categories=", len(self.categories))
# if category tree is empty, initialize it
if not self.categories:
__print__(dbg.PROC, self.module, "→ first_show() → reload_categories()");
try:
self.reload_categories()
except:
__print__(dbg.ERR, "HTTP error or extraction failure.")
self.categories = ["empty"]
self.display_categories()
# Select first category
self.current = self.str_from_struct(self.categories) or None
__print__(dbg.STAT, self.module, "→ first_show(); use first category as current =", self.current)
try:
self.load(self.current)
except:
pass
# put selection/cursor on last position
__print__(dbg.STAT, self.module+".first_show()", "select last known category treelist position =", self.shown)
try:
self.gtk_list.get_selection().select_path(self.shown)
except:
pass
# Invoke only once
self.shown = 55555
# Retrieve first list value, or key from dict (-- used to get first category on init)
def str_from_struct(self, d):
if isinstance(d, (str)):
return d
elif isinstance(d, (dict)):
return self.str_from_struct(d.keys()) or self.str_from_struct(d.values())
elif isinstance(d, (list, tuple)):
return d[0] if len(d) else None
# update categories, save, and display
def reload_categories(self):
# get data and save
self.update_categories()
|
︙ | | | ︙ | |
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
|
"ogg":"ogg", "ogm":"ogg", "xiph":"ogg", "vorbis":"ogg", "vnd.xiph.vorbis":"ogg",
"mp3":"mpeg", "mp":"mpeg", "mp2":"mpeg", "mpc":"mpeg", "mps":"mpeg",
"aac+":"aac", "aacp":"aac",
"realaudio":"x-pn-realaudio", "real":"x-pn-realaudio", "ra":"x-pn-realaudio", "ram":"x-pn-realaudio", "rm":"x-pn-realaudio",
# yes, we do video
"flv":"video/flv", "mp4":"video/mp4",
}
map.update(action.lt) # list type formats (.m3u .pls and .xspf)
if map.get(s):
s = map[s]
# add prefix:
if s.find("/") < 1:
s = "audio/" + s
#
return s
|
|
|
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
|
"ogg":"ogg", "ogm":"ogg", "xiph":"ogg", "vorbis":"ogg", "vnd.xiph.vorbis":"ogg",
"mp3":"mpeg", "mp":"mpeg", "mp2":"mpeg", "mpc":"mpeg", "mps":"mpeg",
"aac+":"aac", "aacp":"aac",
"realaudio":"x-pn-realaudio", "real":"x-pn-realaudio", "ra":"x-pn-realaudio", "ram":"x-pn-realaudio", "rm":"x-pn-realaudio",
# yes, we do video
"flv":"video/flv", "mp4":"video/mp4",
}
#map.update(action.listfmt_t) # list type formats (.m3u .pls and .xspf)
if map.get(s):
s = map[s]
# add prefix:
if s.find("/") < 1:
s = "audio/" + s
#
return s
|
︙ | | | ︙ | |