NetRexx logo
   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    
If you feel inclined to make corrections, suggestions etc., please mail me any.
All content © Ruurd Idenburg, 2007–, except where marked otherwise. All rights reserved. This page is primarily for non-commercial use only. The Idenburg website records no personal information and sets no ‘cookies’. This site is hosted on a VPS(Virtual Private System) rented from Transip.nl, a Dutch company, falling under Dutch (privacy) laws (I think).

This page updated on by Ruurd Idenburg.