'----------------------------------------------------------------------------------------------- ' ' Graphical GTK frontend for the 'mpg123' player ported from Motif. This player only accepts plain http. ' ' Requires BaCon 4.0.1 or higher. May 2020, Peter van Eerten - MIT License. ' ' Version 1.0: initial release ' Version 1.1: code improvements ' Version 1.2: code update to handle hex pointer ' Version 1.3: more code updates ' Version 1.4: set correct input focus in Edit panel ' Version 1.5: fixed bug in parsing URL when fetching title ' Version 1.6: code updates ' Version 1.7: more code updates ' Version 1.8: added pointer cast so it works for 32bit too ' Version 1.9: do not popup with a channel selected ' Version 1.10: more stability in network code ' Version 1.11: added timeout for mpg123 ' Version 1.12: better title tracking ' '----------------------------------------------------------------------------------------------- OPTION GUI TRUE PRAGMA GUI gtk3 OPTION COLLAPSE TRUE OPTION SOCKET 1 CONST Conf$ = GETENVIRON$("HOME") & "/.radio.cfg" CONST Fifo$ = "/dev/shm/mpg.fifo." & STR$(MYPID) CONST Ipc$ = "/dev/shm/mpg.ipc" CONST Buffer = 32768 DECLARE Current_Station$ DECLARE Current_Url$ DECLARE Current_Window SIGNAL SIG_IGN, SIGCHLD TRAP SYSTEM '----------------------------------------------------------------------------------------------- SUB My_Update LOCAL config$, item$, url$, total$ LOCAL x, count, pos, mynet, area, ch IF FILEEXISTS(Ipc$) THEN CALL GUISET(Current_Window, "playing", "label", LOAD$(Ipc$)) IF LEN(Current_Url$) AND COUNT(Current_Url$, ASC("/")) > 2 THEN pid = FORK IF pid = 0 THEN url$ = INBETWEEN$(Current_Url$, "http://", "/") area = MEMORY(Buffer*2) OPEN url$ & ":80" FOR NETWORK AS mynet SEND "GET /" & OUTBETWEEN$(Current_Url$, "url=http://", "/") & " HTTP/1.1\r\nHost: " & url$ & "\r\nUser-Agent: Mozilla/5.0\r\nConnection: keep-alive\r\nIcy-Metadata: 1\r\n\r\n" TO mynet WHILE WAIT(mynet, 1000) RECEIVE area+pos FROM mynet CHUNK Buffer SIZE amount INCR pos, amount IF amount = 0 OR pos > Buffer THEN BREAK WEND CLOSE NETWORK mynet FOR x = 0 TO Buffer-1 ch = PEEK(area+x) IF ch BETWEEN 32 AND 126 THEN total$ = total$ & CHR$(ch) NEXT OPTION QUOTED FALSE title$ = INBETWEEN$(total$, "StreamTitle=", ";") IF LEN(CHOP$(title$, "'")) THEN SAVE CHOP$(title$, "'") TO Ipc$ ELIF NOT(FILEEXISTS(Ipc$)) THEN SAVE "No track info provided by " & TOKEN$(Current_Station$, 2, "=") TO Ipc$ ENDIF FREE area ENDFORK ENDIF ENDIF ALARM My_Update, 3000 ENDSUB '----------------------------------------------------------------------------------------------- SUB Load_Config(id) LOCAL config$, station$, name$, url$ LOCAL (*foreach)() = gtk_container_foreach TYPE void LOCAL (*add)() = gtk_list_box_insert TYPE void CALL GUIFN(id, "list", foreach, (GtkCallback)gtk_widget_destroy, NULL) IF FILEEXISTS(Conf$) THEN config$ = LOAD$(Conf$) FOR station$ IN config$ STEP "#" IF LEN(CHOP$(station$)) = 0 THEN CONTINUE name$ = TOKEN$(CHOP$(station$), 1, NL$) url$ = TOKEN$(CHOP$(station$), 2, NL$) CALL GUIFN(id, "list", add, gtk_widget_new(GTK_TYPE_LABEL, "xalign", 0.0, "label", TOKEN$(name$, 2, "="), NULL), -1) NEXT ENDIF ENDSUB '----------------------------------------------------------------------------------------------- FUNCTION Create_Gui LOCAL window window = GUIDEFINE(" \ { type=WINDOW name=window callback=delete-event title=\"BaCon Internet Radio v1.12\" height-request=300 width-request=450 } \ { type=BOX name=box1 parent=window spacing=3 orientation=GTK_ORIENTATION_VERTICAL } \ { type=FRAME name=frame1 parent=box1 label=\"Radio Stations\" } \ { type=SCROLLED_WINDOW name=swin parent=frame1 expand=TRUE margin=5 valign=GTK_ALIGN_BASELINE callback=button_press_event,popup } \ { type=LIST_BOX name=list parent=swin callback=row-selected selection-mode=GTK_SELECTION_NONE } \ { type=FRAME name=frame2 parent=box1 label=\"Currently Playing\" } \ { type=LABEL name=playing parent=frame2 margin=5 valign=GTK_ALIGN_BASELINE xalign=0.0 ellipsize=PANGO_ELLIPSIZE_END label=\"Use right mouse button for menu...\" } \ { type=MENU name=menu visible=TRUE } \ { type=MENU_ITEM name=Pause parent=menu callback=activate visible=TRUE label=\"Pause\" } \ { type=MENU_ITEM name=Resume parent=menu callback=activate visible=TRUE label=\"Resume\" } \ { type=SEPARATOR_MENU_ITEM name=sep1 parent=menu visible=TRUE } \ { type=MENU_ITEM name=Add parent=menu callback=activate visible=TRUE label=\"Add\" } \ { type=MENU_ITEM name=Edit parent=menu callback=activate visible=TRUE label=\"Edit\" } \ { type=MENU_ITEM name=Delete parent=menu callback=activate visible=TRUE label=\"Delete\" } \ { type=SEPARATOR_MENU_ITEM name=sep2 parent=menu visible=TRUE } \ { type=MENU_ITEM name=Exit parent=menu callback=activate visible=TRUE label=\"Exit\" } \ { type=WINDOW name=input width-request=300 title=\"Radio Station\" } \ { type=BOX name=box2 parent=input spacing=3 orientation=GTK_ORIENTATION_VERTICAL } \ { type=LABEL name=label1 parent=box2 xalign=0.0 label=\"Radio Station Name:\" } \ { type=ENTRY name=en1 parent=box2 margin=5 } \ { type=LABEL name=label2 parent=box2 xalign=0.0 label=\"Radio Station URL:\" } \ { type=ENTRY name=en2 parent=box2 margin=5 } \ { type=SEPARATOR name=sep parent=box2 orientation=GTK_ORIENTATION_HORIZONTAL } \ { type=BUTTON_BOX name=bbox parent=box2 layout-style=GTK_BUTTONBOX_EDGE } \ { type=BUTTON name=OK parent=bbox callback=clicked margin=5 label=\"OK\" } \ { type=BUTTON name=Cancel parent=bbox callback=clicked margin=5 label=\"Cancel\" } \ { type=MESSAGE_DIALOG name=err1 callback=delete-event callback=response,yesno message-type=GTK_MESSAGE_WARNING buttons=GTK_BUTTONS_YES_NO title=\"Delete Station\" text=\"Are you sure?\" } \ { type=MESSAGE_DIALOG name=err2 callback=delete-event callback=response,errok message-type=GTK_MESSAGE_ERROR buttons=GTK_BUTTONS_OK title=\"Warning\" text=\"No 'mpg123' found on this system!\" }") CALL Load_Config(window) Current_Window = window RETURN window ENDFUNCTION '----------------------------------------------------------------------------------------------- SUB Main_Function(id) LOCAL event TYPE GdkEventButton* LOCAL resp TYPE int* LOCAL event$, name$, url$, status$, config$, item$ LOCAL name, url TYPE char* LOCAL pos, mode LOCAL (*show)() = gtk_widget_show_all TYPE void LOCAL (*hide)() = gtk_widget_hide TYPE void LOCAL (*popup)() = gtk_menu_popup_at_pointer TYPE void IF FILEEXISTS(Ipc$) THEN DELETE FILE Ipc$ CALL My_Update() WHILE TRUE event$ = GUIEVENT$(id, TRUE) CALL GUIGET(id, "list", "selection-mode", &mode) IF mode = GTK_SELECTION_NONE THEN CALL GUISET(id, "list", "selection-mode", GTK_SELECTION_SINGLE) SELECT TOKEN$(event$, 1) CASE "Add" CALL GUIFN(id, "input", show) CALL GUISET(id, "en1", "text", "") CALL GUISET(id, "en2", "text", "") CALL GUISET(id, "en1", "has-focus", TRUE) status$ = "added" CASE "Edit" CALL GUIFN(id, "input", show) pos = gtk_list_box_row_get_index(gtk_list_box_get_selected_row(GTK_LIST_BOX(GUIWIDGET(id, "list"))))+1 config$ = LOAD$(Conf$) item$ = CHOP$(TOKEN$(config$, pos, "#")) name$ = TOKEN$(item$, 1, NL$) url$ = TOKEN$(item$, 2, NL$) CALL GUISET(id, "en1", "text", TOKEN$(name$, 2, "=")) CALL GUISET(id, "en2", "text", TOKEN$(url$, 2, "=")) status$ = "edited" CASE "Delete" CALL GUIFN(id, "err1", show) CASE "window", "Exit" IF FILEEXISTS(Fifo$) THEN APPEND "QUIT" & NL$ TO Fifo$ IF FILEEXISTS(Ipc$) THEN DELETE FILE Ipc$ END CASE "OK" config$ = LOAD$(Conf$) IF status$ = "edited" THEN pos = gtk_list_box_row_get_index(gtk_list_box_get_selected_row(GTK_LIST_BOX(GUIWIDGET(id, "list"))))+1 config$ = CHOP$(DEL$(config$, pos, "#")) ENDIF CALL GUIGET(id, "en1", "text", &name) CALL GUIGET(id, "en2", "text", &url) config$ = CHOP$(config$) & NL$ & "name=" & name & NL$ & "url=" & url & NL$ & "#" & NL$ SAVE SORT$(config$, "#" & NL$) & "#" & NL$ TO Conf$ CALL GUIFN(id, "input", hide) Load_Config(id) CASE "Cancel" CALL GUIFN(id, "input", hide) CASE "err1" CALL GUIFN(id, "err1", hide) CASE "yesno" resp = (intptr_t)DEC(TOKEN$(event$, 2)) IF *resp = GTK_RESPONSE_YES THEN pos = gtk_list_box_row_get_index(gtk_list_box_get_selected_row(GTK_LIST_BOX(GUIWIDGET(id, "list"))))+1 config$ = LOAD$(Conf$) config$ = CHOP$(DEL$(config$, pos, "#")) SAVE config$ & NL$ TO Conf$ Load_Config(id) ENDIF CALL GUIFN(id, "err1", hide) CASE "popup" event = (intptr_t)DEC(TOKEN$(event$, 2)) IF event->type = GDK_BUTTON_PRESS AND event->button = GDK_BUTTON_SECONDARY THEN CALL GUIFN(id, "menu", popup, (GdkEvent*)event) CASE "list", "Resume" IF NOT(LEN(EXEC$("command -v mpg123 2>/dev/null"))) THEN CALL GUIFN(id, "err2", show) ELSE IF FILEEXISTS(Ipc$) THEN DELETE FILE Ipc$ IF NOT(FILEEXISTS(Fifo$)) THEN SYSTEM "mpg123 -o pulse,openal,alsa -R --fifo " & Fifo$ & " --timeout 1 >/dev/null 2>&1 &" REPEAT SLEEP 50 UNTIL FILEEXISTS(Fifo$) pos = gtk_list_box_row_get_index(gtk_list_box_get_selected_row(GTK_LIST_BOX(GUIWIDGET(id, "list"))))+1 config$ = LOAD$(Conf$) item$ = CHOP$(TOKEN$(config$, pos, "#")) url$ = TOKEN$(item$, 2, NL$) APPEND "LOAD " & TOKEN$(url$, 2, "=") & NL$ TO Fifo$ Current_Url$ = url$ Current_Station$ = TOKEN$(item$, 1, NL$) CALL GUISET(id, "playing", "label", "Fetching track info...") ENDIF CASE "Pause" IF FILEEXISTS(Fifo$) THEN APPEND "QUIT" & NL$ TO Fifo$ CASE "errok", "err2" CALL GUIFN(id, "err2", hide) ENDSELECT WEND ENDSUB '----------------------------------------------------------------------------------------------- Main_Function(Create_Gui()) '-----------------------------------------------------------------------------------------------