1: #!/usr/bin/env rexx
2:
3: --parse arg gladeFile
4: guiFile = "/usr/local/gui/radio.ui"
5: pipeFile = "~/.local/share/internetradio/fifo"
6: radioDir = "~/.local/share/internetradio"
7: radioFile = radioDir"/radio.cfg"
8: helpFile = "/usr/local/bin/internetRadio.hlp"
9:
10: -- check existance or create ~/.local/share/intenetradio
11: -- for storing the list of radio stations
12: address system 'mkdir -p' radioDir
13:
14: -- initialize RexxGTK
15: call gtk_init
16:
17: -- check musicplayer availability
18: music_player = "mpg123"
19: if (check_executable_available(music_player)=.false) then exit
20:
21: -- check availability of url scanner
22: urlscanner = "wget"
23: if (check_executable_available(urlscanner)=.false) then exit
24:
25: --start the music player
26: if (start_music_player(pipeFile, music_player)=.false) then exit
27:
28: -- build the user interface from a Glade file
29: builder = .GtkBuilder~new_from_file(guiFile)
30:
31: -- get access to the top level window.
32: window = .mainWindow~from_builder(builder,"WINDOW")
33: window~set_title("RexxGTK - MP3 Internet Radio Player")
34:
35: --get access to the scrolled window
36: scrolledWindow = .GtkScrolledWindoW~from_builder(builder,"STATIONS")
37:
38: -- load the array with saved radio stations from .radio.cfg
39: window~radioFile = radioFile
40: call load_stations window
41:
42: -- create the Treeview to be used for the stations
43: window~stationView = .stationView~new
44:
45: -- create the Treeview columns (i.e. station's name and url
46: call setup_tree_view window~stationView, window~COL_NAME, window~COL_URL
47:
48: -- set selection to single row
49: selection = window~stationView~get_selection
50: selection~set_mode(.gtk~GTK_SELECTION_SINGLE);
51:
52: --selection~connect_signal("changed")
53: window~selection = selection
54:
55: -- create the Liststore for the radio stations
56: store = .GtkListStore~new(.gtk~G_TYPE_STRING, .gtk~G_TYPE_STRING)
57: window~listStore = store
58:
59: -- fill the listStore wih the stations
60: call add_stations_to_list window
61:
62: -- set the Liststore as the model for the radioView
63: window~stationView~set_model(store)
64: window~stationView~user_data = window
65:
66: -- add the stationView to the scrolled window
67: scrolledWindow~add(window~stationView)
68:
69: -- get access to the quitButton to exit the program
70: quitButton = .quitButton~from_builder(builder, "QUIT")
71:
72: -- get access to the aboutButton provide some info
73: aboutButton = .aboutButton~from_builder(builder, "ABOUT")
74:
75: --get access to the helpButton to get some instructions
76: helpButton = .helpButton~from_builder(builder, "README")
77:
78: -- get access to the playButton to stream a radiostation
79: playButton = .playButton~from_builder(builder, "PLAY")
80:
81: -- get access to the pauseButton to stop streaming
82: pauseButton = .pauseButton~from_builder(builder, "PAUSE")
83:
84: -- get access to addButton to add a new radiostation
85: addButton = .addButton~from_builder(builder, "ADD")
86: addButton~user_data = window
87:
88: -- get access to the editButton to change a radiostation
89: editButton = .editButton~from_builder(builder, "EDIT")
90: editButton~user_data = window
91:
92: -- get access to the deleteButton to delete a radiostation
93: deleteButton = .deleteButton~from_builder(builder, "DEL")
94:
95: -- get access to the titleLabel to display music titles
96: streamLabel = .gtkLabel~from_builder(builder,"LABEL")
97: streamLabel~set_label("Hello")
98: window~streamLabel = streamLabel
99:
100: -- start the ticker to update streamTitle if streaming is active
101: window~streamTicker = .Ticker~new(10,.streamScanner~new,window)
102:
103: -- get access to add an internet station dialog
104: window~radioDialog = .GtkDialog~from_builder(builder,"CONFIG")
105:
106: -- get access the entries for a radiostation
107: window~nameEntry = .GtkEntry~from_builder(builder,"ADD_NAME")
108: window~urlEntry = .GtkEntry~from_builder(builder,"ADD_URL")
109:
110: --set other window attributes
111: window~pipeFile = pipeFile
112: window~helpFile = helpFile
113: window~streaming = .false
114: window~radioName = window~nameEntry~get_text()
115: window~radioURL = window~urlEntry~get_text()
116: window~streamTitle = streamLabel~get_label
117:
118: -- set all needed user_data
119: quitButton~user_data = window
120: helpButton~user_data = window
121: deleteButton~user_data = window
122: playButton~user_data = window
123: pauseButton~user_data = window
124: editButton~user_data = window
125:
126: --connect the wanted signals to the widgets
127: window~signal_connect("destroy")
128: quitButton~signal_connect("clicked")
129: aboutButton~signal_connect("clicked")
130: helpButton~signal_connect("clicked")
131: playButton~signal_connect("clicked")
132: pauseButton~signal_connect("clicked")
133: addButton~signal_connect("clicked")
134: editButton~signal_connect("clicked")
135: deleteButton~signal_connect("clicked")
136: window~stationView~signal_connect("row_activated")
137:
138: -- for the dialog buttons
139: --addOkButton~signal_connect("clicked")
140: --addCancelButton~signal_connect("clicked")
141:
142: -- display everything
143: window~show_all()
144: -- do the GTK interaction loop
145: call gtk_main
146: exit
147:
148: ::requires 'oorexxgtk3.cls'
149:
150: /*===========================================================================*/
151: /* mainWindow */
152: /*===========================================================================*/
153: ::class MainWindow subclass GtkWindow
154:
155: ::constant COL_NAME 0
156: ::constant COL_URL 1
157: ::constant COLUMNS 2
158: --objects
159: ::attribute radioDialog
160: ::attribute nameEntry
161: ::attribute urlEntry
162: ::attribute selection
163: ::attribute radioStations
164: ::attribute stationView
165: ::attribute listStore
166: ::attribute streamLabel
167: -- strings
168: ::attribute pipeFile
169: ::attribute radioFile
170: ::attribute helpFile
171: ::attribute radioName
172: ::attribute radioURL
173: ::attribute streamTicker
174: ::attribute streamTitle
175: ::attribute streaming
176:
177: ::method signal_destroy
178: -- we need to cancel the streamTicker or this application will still hang around
179: self~streamTicker~cancel
180: -- we also need to stop the music player
181: address system "echo QUIT >>"self~pipeFile
182: --call lineOut self~pipeFile,"QUIT"
183: call gtk_main_quit
184: return
185:
186: /*===========================================================================*/
187: /* streamScanner */
188: /*===========================================================================*/
189:
190: ::class streamScanner subclass AlarmNotification
191: ::method triggered
192: --trace i
193: use strict arg scanner
194: window = scanner~attachment
195: if window~streaming then do
196: title = get_title_from_stream(window)
197: window~streamTitle = title
198: window~streamLabel~set_label(title~left(50))
199: --say title
200: end
201: exit
202:
203: /*===========================================================================*/
204: /* stationView */
205: /*===========================================================================*/
206:
207: ::class stationView subclass GtkTreeView
208: ::method 'signal_row_activated'
209: --trace i
210: say self 'row_activated'
211: window = self~user_data
212: currentRadio = window~radioName
213: --say currentRadio
214: treePath = .GtkTreePath~new(arg(1))
215: model = self~get_model
216: iter = model~get_iter(treePath)
217: window~radioName = model~get_value(iter,0)
218: window~radioURL = model~get_value(iter,1)
219: --say window~radioName
220: say '"'window~radioURL'"'
221: address system "echo LOAD" window~radioURL ">>"window~pipeFile
222: --lineOut(window~pipeFile,"LOAD" window~radioURL)
223: window~streaming = .true
224: window~streamTitle = get_title_from_stream(window)
225: --say window~streamTitle
226: window~streamLabel~set_label(window~streamTitle~left(50))
227: return .true
228:
229: /*===========================================================================*/
230: /* quitButton */
231: /*===========================================================================*/
232: ::class quitButton subclass GtkButton
233: ::method signal_clicked
234: --trace i
235: say self 'clicked'
236: window = self~user_data
237: -- we need to stop the streamTicker
238: window~streamTicker~cancel
239: -- and stop the music player
240: address system "echo QUIT >>"window~pipeFile
241: --call lineOut window~pipeFile,"QUIT"
242: call gtk_main_quit
243: return .true
244:
245: /*===========================================================================*/
246: /* aboutButton */
247: /*===========================================================================*/
248: ::class aboutButton subclass GtkButton
249: ::method signal_clicked
250: --trace i
251: say self 'clicked'
252: dialog = .GtkAboutDialog~new()
253: logo = 'img/radio.png'
254: dialog~set_logo(logo)
255: dialog~set_name('RexxGTK Internet Radio')
256: dialog~set_version('1.0.0')
257: dialog~set_copyright('(c) None, no rights reserved')
258: dialog~set_comments('Listen to internet radio stations.')
259: dialog~set_license('No copyright, no licence, no guarantees or warrantees, be it' || .string~nl ,
260: 'explicit, implicit or whatever. Usage is totally and completely' || .string~nl ,
261: 'at the users own risk, the author shall not be liable for any' || .string~nl ,
262: 'damages whatsoever for any reason whatsoever.')
263: dialog~set_website('http://www.oorexx.org/')
264: dialog~set_website_label('ooRexx Web Site')
265: dialog~set_authors('W. David Ashley - The original RexxGTK effort', ,
266: 'Peter van Eerden - The program design for his gtk-server', ,
267: 'Ruurd J. Idenburg - The adaptation to RexxGTK')
268: dialog~show_all()
269: dialog~dialog_run()
270: dialog~destroy()
271: return .true
272:
273: /*===========================================================================*/
274: /* helpButton */
275: /*===========================================================================*/
276: ::class helpButton subclass GtkButton
277: ::method signal_clicked
278: --trace i
279: say self 'clicked'
280: window = self~user_data
281: call 'helpInfo.rex' window~helpFile
282: return .true
283:
284: /*===========================================================================*/
285: /* playButton */
286: /*===========================================================================*/
287: ::class playButton subclass GtkButton
288: ::method signal_clicked
289: --trace i
290: say self 'clicked'
291: window = self~user_data
292: if \window~streaming then do
293: window~streaming = .true
294: address system "echo PAUSE >>"window~pipeFile
295: end
296: return .true
297:
298: /*===========================================================================*/
299: /* pauseButton */
300: /*===========================================================================*/
301: ::class pauseButton subclass GtkButton
302: ::method signal_clicked
303: --trace i
304: say self 'clicked'
305: window = self~user_data
306: if window~streaming then do
307: window~streaming = .false
308: address system "echo PAUSE >>"window~pipeFile
309: end
310: return .true
311:
312: /*===========================================================================*/
313: /* addButton */
314: /*===========================================================================*/
315: ::class addButton subclass GtkButton
316: ::method signal_clicked
317: --trace i
318: say self 'clicked'
319: -- user_data should be the main window
320: window = self~user_data
321: -- blank both addDialog entry fields
322: window~nameEntry~set_text("")
323: window~urlEntry~set_text("")
324: dlg = window~radioDialog
325: dlg~show_all
326: returncode = dlg~dialog_run()
327: if returnCode=.gtk~GTK_RESPONSE_OK then do
328: say 'Ok' returnCode
329: name = window~nameEntry~get_text()
330: URL = window~urlEntry~get_text()
331: window~radioStations~append(name';'URL)
332: call save_stations window
333: call load_stations window
334: call add_stations_to_list window
335: window~radioName = name
336: window~radioURL = URL
337: end
338: if returncode=.gtk~GTK_RESPONSE_CANCEL then do
339: say 'Cancel' returnCode
340: end
341: dlg~set_visible(.false)
342: return .true
343:
344: /*===========================================================================*/
345: /* editButton */
346: /*===========================================================================*/
347: ::class editButton subclass GtkButton
348: ::method signal_clicked
349: --trace i
350: say self 'clicked'
351: window = self~user_data
352: nameEntry = window~nameEntry
353: urlEntry = window~urlEntry
354: nameEntry~set_text(window~radioName)
355: urlEntry~set_text(window~radioURL)
356: --dlg~set_title("Edit Radio Dialog")
357: dlg = window~radioDialog
358: dlg~show_all
359: returncode = dlg~dialog_run()
360: -- dialog responded with OK
361: if returnCode=.gtk~GTK_RESPONSE_OK then do
362: say 'Ok' returnCode
363: currentName = window~radioName
364: newName = nameEntry~get_text()~strip
365: currentURL = window~radioURL
366: newURL = urlEntry~get_text()
367: if newName<>currentName | newURL<>currentURL then do
368: --delete current
369: loop i=1 to window~radioStations~items
370: parse value window~radioStations[i] with stationName ";" url
371: if currentName=stationName then do
372: window~radioStations~delete(i)
373: leave
374: end
375: end
376: --add new
377: window~radioStations~append(newName';'newURL)
378: call save_stations window
379: call load_stations window
380: call add_stations_to_list window
381: end
382: window~radioName = newName
383: window~radioURL = newURL
384: end
385: -- dialog responded with cancel, nothing has to changed
386: if returncode=.gtk~GTK_RESPONSE_CANCEL then do
387: say 'Cancel' returnCode
388: end
389: dlg~set_visible(.false)
390: return .true
391:
392: /*===========================================================================*/
393: /* deleteButton */
394: /*===========================================================================*/
395: ::class deleteButton subclass GtkButton
396: ::method signal_clicked
397: say self 'clicked'
398: --trace i
399: window = self~user_data
400: view = window~stationView
401: selection = window~selection
402: model = view~get_model
403: iter = model~get_iter_from_string("0")
404: flag = selection~get_selected(model,iter)
405: if flag then do
406: name = model~get_value(iter,0)
407: loop i=1 to window~radioStations~items
408: parse value window~radioStations[i] with stationName ";" url
409: if name=stationName then do
410: window~radioStations~delete(i)
411: leave
412: end
413: end
414: call save_stations window
415: call load_stations window
416: call add_stations_to_list window
417: end
418: return .true
419:
420: /*===========================================================================*/
421: /* addOkButton */
422: /*===========================================================================*/
423:
424: ::class addOkButton subclass GtkButton
425: ::method signal_clicked
426: --trace i
427: say self 'clicked'
428: return .true
429:
430: /*===========================================================================*/
431: /* addCancelButton */
432: /*===========================================================================*/
433:
434: ::class addCancelButton subclass GtkButton
435: ::method signal_clicked
436: --trace i
437: say self 'clicked'
438: return .true
439:
440: /*===========================================================================*/
441: /* setup_tree_view */
442: /*===========================================================================*/
443:
444: ::routine setup_tree_view
445: use strict arg treeview, COL_NAME, COL_URL
446:
447: renderer = .GtkCellRendererText~new()
448: column = .GtkTreeViewColumn~newWithAttributes('Name', renderer, 'text', COL_NAME)
449: treeview~append_column(column)
450:
451: renderer = .GtkCellRendererText~new()
452: column = .GtkTreeViewColumn~newWithAttributes('URL', renderer, 'text', COL_URL)
453: column~set_visible(.false)
454: treeview~append_column(column)
455: treeview~set_activate_on_single_click(1)
456: return
457:
458: /*===========================================================================*/
459: /* load_stations */
460: /*===========================================================================*/
461:
462: -- setup of radiostations known from previous sessions
463: -- or if none, setup the ones from the ::resource directive
464: -- saved stations are stored in the directory specified
465: -- at the beginning as file:"radio.cfg"
466: ::routine load_stations
467: --trace i
468: use strict arg window
469: radioConfig = window~radioFile
470: if radioConfig<>.nil then do
471: radioStream = .Stream~new(radioConfig)~~open()
472: if radioStream~lines<>0 then do
473: radioStations = radioStream~arrayIn()
474: radioStations~sort
475: window~radioStations = radioStations
476: end
477: else do -- if no stations yet set some
478: radioStations = .resources[radio_stations]
479: window~radioStations = radioStations
480: call save_stations window
481: end
482: end
483: return
484:
485: /*===========================================================================*/
486: /* add_stations_to_list */
487: /*===========================================================================*/
488:
489: -- Add the stations from the array to the list
490: ::routine add_stations_to_list
491: --trace i
492: use strict arg window
493: listStore = window~listStore
494: listStore~clear
495: do station over window~radioStations
496: parse var station name ';' url
497: iter = listStore~append()
498: listStore~set_value(iter, window~col_name, name, window~col_url, url)
499: end
500: return
501:
502: /*===========================================================================*/
503: /* save_stations */
504: /*===========================================================================*/
505:
506: ::routine save_stations
507: use strict arg window
508: --trace i
509: -- get the .radio.cfg file
510: radioFile = window~radioFile
511: -- all known stations are kept in the mainWwindow .Array attribute "radioStations"
512: stations = window~radioStations
513: -- open the .radio.cfg file wih the "Replace option to overwrite an existing file
514: radioStream = .Stream~new(radioFile)~~open('Replace')
515: -- write the stations array to the file
516: radioStream~arrayOut(stations)
517: -- and close the file
518: radioStream~close
519: return
520:
521: /*===========================================================================*/
522: /* check_executable_available */
523: /*===========================================================================*/
524:
525: ::routine check_executable_available
526: use strict arg executable
527: address system "which" executable with output stem res.
528: -- if not found res.0 will be zero
529: if res.0=0 then do
530: -- display a modal error message
531: flags = .gtk~GTK_DIALOG_MODAL
532: type = .gtk~GTK_MESSAGE_ERROR
533: buttons = .gtk~GTK_BUTTONS_CLOSE
534: msg = "Can not locate "executable". Please install it, e.g. (sudo) apt install "executable
535: errorDialog = .GtkMessageDialog~new(.nil,flags,type,buttons,msg)
536: errorDialog~dialog_run
537: errorDialog~destroy
538: return .false
539: end
540: return .true
541:
542: /*===========================================================================*/
543: /* get_title_from_stream */
544: /*===========================================================================*/
545: /**
546: * We are scanning the currently streaming station for the title of the music piece.
547: * The title may (or may not) be found within the stream because of the header we give.
548: * If the streaming station suport the metadata within the stream, then it normally is
549: * inserted within any 65535 characters, so we scan for that amount of bytes. We catch
550: * printable characters within the stream with the 'strings' command and finally see
551: * if the streamtitle is found within the strings-found lines with the 'grep' command.
552: *
553: *
554: **/
555: ::routine get_title_from_stream
556: use strict arg window
557: --when only do this when we are really streaming
558: if window~streaming then do
559: -- timeout 1 second to establish comnnection with the url
560: to = "--timeout=1"
561: -- if unsuccessfull just 1 retry
562: tr = "--tries=1"
563: -- we are interested in the metadata witin the stream
564: hdr = '--header="Icy-Metadata: 1"'
565: -- the address of the streaming radio station
566: wa = window~radioURL
567: -- output to stdin stderr to nowhere
568: out = "-O - 2>/dev/null"
569: -- the pipe to filter the the title of the piece of music being streamed
570: pipe = "| head -c 65535 | strings | grep 'StreamTitle='"
571: address system "wget" to tr hdr wa out pipe with output stem res.
572: if res.0 = 0
573: then title = wa
574: else parse var res.1 . "='" title "';"
575: end
576: return title
577:
578: ::routine start_music_player
579: use strict arg pipe, player
580: if player="mpg123" then do
581: address system "rm" pipe "2>/dev/null"
582: address system "mpg123 -o pulse,alsa,openal -R --fifo" pipe ">/dev/null 2>&1 &"
583: address bash "while [ ! -p" pipe "]; do continue; done"
584: return .true
585: end
586: else do
587: -- display a modal error message
588: flags = .gtk~GTK_DIALOG_MODAL
589: type = .gtk~GTK_MESSAGE_ERROR
590: buttons = .gtk~GTK_BUTTONS_CLOSE
591: msg = "No support available for '"player"'! Exiting ....."
592: errorDialog = .GtkMessageDialog~new(.nil,flags,type,buttons,msg)
593: errorDialog~dialog_run
594: errorDialog~destroy
595: return .false
596: end
597:
598: ::resource radio_stations
599: BBC World Service;http://utulsa.streamguys1.com/KWGSHD3-MP3
600: Deutschlandfunk;http://st01.sslstream.dlf.de/dlf/01/128/mp3/stream.mp3
601: MotherEarth Radio - Classical;http://motherearth.streamserver24.com:18910/motherearth.klassik.mp3
602: MotherEarth Radio - Jazz;http://motherearth.streamserver24.com/listen/motherearth_jazz/motherearth.jazz.mp3
603: NPO Radio 2 Popular;http://icecast.omroep.nl:80/radio2-bb-mp3
604: NPO Radio 4 Classical;http://icecast.omroep.nl:80/radio4-bb-mp3
605: NPO Radio 6 Jazz & Soul;http://icecast.omroep.nl:80/radio6-bb-mp3
606: NPR News and Talk;http://npr-ice.streamguys1.com/live.mp3
607: Radio Caroline;http://sc6.radiocaroline.net:8040/mp3
608: Radio Paradise Mellow;http://stream.radioparadise.com/mellow-192
609: Radio Paradise Rock;http://stream.radioparadise.com/rock-192
610: Royal Concertgebouw Orchestra;http://ice.cr5.streamzilla.xlcdn.com:8000/sz=RCOLiveWebradio=mp3-192
611: ::END