'----------------------------------------------------------------------------------------------- ' ' Graphical GTK frontend for the 'mpg123' player ported from GTK3. This player only accepts plain http. ' ' Requires BaCon 4.3.2 or higher. April 2024, Peter van Eerten - MIT License. ' ' Version 1.0: initial release for GTK4 ' Version 1.1: better title tracking ' '----------------------------------------------------------------------------------------------- OPTION GUI TRUE PRAGMA GUI gtk4 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 LOCAL (*show)() = gtk_widget_show TYPE void LOCAL (*hide)() = gtk_widget_hide TYPE void DECLARE (*row_get)() = gtk_list_box_get_selected_row TYPE GtkListBoxRow* SIGNAL SIG_IGN, SIGCHLD '----------------------------------------------------------------------------------------------- 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 (*row_idx)() = gtk_list_box_get_row_at_index TYPE GtkListBoxRow* LOCAL (*add)() = gtk_list_box_insert TYPE void LOCAL (*remove)() = gtk_list_box_remove TYPE void LOCAL *row TYPE GtkWidget IF FILEEXISTS(Conf$) THEN config$ = LOAD$(Conf$) WHILE TRUE row = GUIFN(id, "list", row_idx, 0) IF row THEN CALL GUIFN(id, "list", remove, row) ELSE BREAK ENDIF WEND 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, g_object_new(GTK_TYPE_LABEL, "xalign", 0.0, "label", TOKEN$(name$, 2, "="), NULL), -1) NEXT ENDIF CALL gtk_list_box_unselect_all(GUIWIDGET(id, "list")) ENDSUB '----------------------------------------------------------------------------------------------- SUB Play(id) LOCAL url$, config$, item$ LOCAL pos IF NOT(LEN(EXEC$("command -v mpg123 2>/dev/null"))) THEN CALL GUIFN(id, "err", 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(GUIFN(id, "list", row_get))+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 ENDSUB '----------------------------------------------------------------------------------------------- SUB Cleanup IF FILEEXISTS(Fifo$) THEN APPEND "QUIT" & NL$ TO Fifo$ IF FILEEXISTS(Ipc$) THEN DELETE FILE Ipc$ ENDSUB '----------------------------------------------------------------------------------------------- SUB Mouse_Event(GtkGestureClick *gesture, n_press, double x, double y, gpointer user_data) LOCAL rect TYPE GdkRectangle rect.x = INT(x) rect.y = INT(y) rect.width = 0 rect.height = 0 CALL gtk_popover_set_pointing_to(GUIWIDGET(Current_Window, "menu"), &rect) CALL gtk_popover_popup(GUIWIDGET(Current_Window, "menu")) ENDSUB FUNCTION Create_Gui LOCAL window LOCAL *gesture TYPE GtkGesture LOCAL (*ctrl_add)() = gtk_widget_add_controller TYPE void LOCAL (*add)() = gtk_list_box_insert TYPE void LOCAL (*parent)() = gtk_widget_set_parent TYPE void LOCAL (*attach)() = gtk_grid_attach TYPE void window = GUIDEFINE(" \ { type=WINDOW name=window callback=close-request title=\"BaCon Internet Radio v1.1 for GTK4\" height-request=300 width-request=450 } \ { type=BOX name=box parent=window spacing=3 orientation=GTK_ORIENTATION_VERTICAL } \ { type=FRAME name=frame1 parent=box label=\"Radio Stations\" } \ { type=SCROLLED_WINDOW name=swin parent=frame1 hexpand=TRUE vexpand=TRUE margin-top=5 margin-bottom=5 margin-start=5 margin-end=5 valign=GTK_ALIGN_BASELINE } \ { type=LIST_BOX name=list parent=swin callback=row-selected selection-mode=GTK_SELECTION_BROWSE } \ { type=FRAME name=frame2 parent=box label=\"Currently Playing\" } \ { type=LABEL name=playing parent=frame2 valign=GTK_ALIGN_BASELINE xalign=0.0 ellipsize=PANGO_ELLIPSIZE_END label=\"Use right mouse button for menu...\" } \ { type=POPOVER name=menu callback=show has-arrow=FALSE } \ { type=LIST_BOX name=list2 parent=menu callback=row-selected selection-mode=GTK_SELECTION_NONE } \ { type=WINDOW name=input width-request=300 title=\"Radio Station\" } \ { type=GRID name=grid parent=input column-spacing=5 row-spacing=5 } \ { type=LABEL name=label1 xalign=0.0 label=\"Radio Station Name:\" } \ { type=ENTRY name=en1 width-request=250 hexpand=TRUE valign=GTK_ALIGN_START } \ { type=LABEL name=label2 xalign=0.0 label=\"Radio Station URL:\" } \ { type=ENTRY name=en2 width-request=250 hexpand=TRUE valign=GTK_ALIGN_START } \ { type=SEPARATOR name=sep orientation=GTK_ORIENTATION_HORIZONTAL } \ { type=BUTTON name=OK callback=clicked vexpand=TRUE valign=GTK_ALIGN_END halign=GTK_ALIGN_START label=\"OK\" } \ { type=BUTTON name=Cancel callback=clicked vexpand=TRUE valign=GTK_ALIGN_END halign=GTK_ALIGN_END label=\"Cancel\" } \ { type=WINDOW name=question_dialog callback=close-request width-request=200 title=\"Delete station\" } \ { type=GRID name=question_grid parent=question_dialog column-spacing=10 row-spacing=10 } \ { type=LABEL name=question_label hexpand=TRUE label=\"Are you sure?\" } \ { type=BUTTON name=question_no callback=clicked label=\" No \" vexpand=TRUE valign=GTK_ALIGN_END halign=GTK_ALIGN_START } \ { type=BUTTON name=question_yes callback=clicked label=\" Yes \" vexpand=TRUE valign=GTK_ALIGN_END halign=GTK_ALIGN_END } \ { type=MESSAGE_DIALOG name=err callback=response transient-for=window 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 ' Populate popup CALL GUIFN(window, "menu", parent, GUIWIDGET(window, "swin")) CALL GUIFN(window, "list2", add, g_object_new(GTK_TYPE_LABEL, "xalign", 0.0, "label", "Pause", NULL), -1) CALL GUIFN(window, "list2", add, g_object_new(GTK_TYPE_LABEL, "xalign", 0.0, "label", "Resume", NULL), -1) CALL GUIFN(window, "list2", add, g_object_new(GTK_TYPE_SEPARATOR, "can-target", 0, NULL), -1) CALL GUIFN(window, "list2", add, g_object_new(GTK_TYPE_LABEL, "xalign", 0.0, "label", "Add", NULL), -1) CALL GUIFN(window, "list2", add, g_object_new(GTK_TYPE_LABEL, "xalign", 0.0, "label", "Edit", NULL), -1) CALL GUIFN(window, "list2", add, g_object_new(GTK_TYPE_LABEL, "xalign", 0.0, "label", "Delete", NULL), -1) CALL GUIFN(window, "list2", add, g_object_new(GTK_TYPE_SEPARATOR, "can-target", 0, NULL), -1) CALL GUIFN(window, "list2", add, g_object_new(GTK_TYPE_LABEL, "xalign", 0.0, "label", "Exit", NULL), -1) ' Setup add/edit window CALL GUIFN(window, "grid", attach, GUIWIDGET(window, "label1"), 0, 0, 2, 1) CALL GUIFN(window, "grid", attach, GUIWIDGET(window, "en1"), 0, 1, 2, 1) CALL GUIFN(window, "grid", attach, GUIWIDGET(window, "label2"), 0, 2, 2, 1) CALL GUIFN(window, "grid", attach, GUIWIDGET(window, "en2"), 0, 3, 2, 1) CALL GUIFN(window, "grid", attach, GUIWIDGET(window, "sep"), 0, 4, 2, 1) CALL GUIFN(window, "grid", attach, GUIWIDGET(window, "OK"), 0, 5, 1, 1) CALL GUIFN(window, "grid", attach, GUIWIDGET(window, "Cancel"), 1, 5, 1, 1) ' Setup question window CALL GUIFN(window, "question_grid", attach, GUIWIDGET(window, "question_label"), 0, 0, 2, 1) CALL GUIFN(window, "question_grid", attach, GUIWIDGET(window, "question_no"), 0, 1, 1, 1) CALL GUIFN(window, "question_grid", attach, GUIWIDGET(window, "question_yes"), 1, 1, 1, 1) ' Define right mouse click gesture = gtk_gesture_click_new() CALL GUIFN(window, "swin", ctrl_add, gesture) CALL gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(gesture), 3) CALL g_signal_connect(G_OBJECT(gesture), "pressed", G_CALLBACK(Mouse_Event), NULL) RETURN window ENDFUNC '----------------------------------------------------------------------------------------------- SUB Main_Function(id) LOCAL resp TYPE int* LOCAL name$, url$, status$, config$, item$ LOCAL name, url TYPE char* LOCAL pos, mode IF FILEEXISTS(Ipc$) THEN DELETE FILE Ipc$ CALL My_Update() WHILE TRUE SELECT GUIEVENT$(id) CASE "menu" CALL GUISET(id, "list2", "selection-mode", GTK_SELECTION_NONE) CALL GUISET(id, "list2", "selection-mode", GTK_SELECTION_BROWSE) CASE "list2" SELECT gtk_list_box_row_get_index(GUIFN(id, "list2", row_get))+1 CASE 1 IF FILEEXISTS(Fifo$) THEN APPEND "QUIT" & NL$ TO Fifo$ CASE 2 Play(id) CASE 4 CALL GUIFN(id, "input", show) CALL GUISET(id, "en1", "text", "") CALL GUISET(id, "en2", "text", "") CALL gtk_widget_grab_focus(GUIWIDGET(id, "en1")) status$ = "added" CASE 5 CALL GUIFN(id, "input", show) pos = gtk_list_box_row_get_index(GUIFN(id, "list", row_get))+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 6 CALL GUIFN(id, "question_dialog", show) CASE 8 Cleanup() END ENDSELECT CALL gtk_popover_popdown(GUIWIDGET(Current_Window, "menu")) CASE "window" Cleanup() END CASE "list" Play(id) CASE "OK" config$ = LOAD$(Conf$) IF status$ = "edited" THEN pos = gtk_list_box_row_get_index(GUIFN(id, "list", row_get))+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 "question_dialog", "question_no" CALL GUIFN(id, "question_dialog", hide) CASE "question_yes" pos = gtk_list_box_row_get_index(GUIFN(id, "list", row_get))+1 config$ = LOAD$(Conf$) config$ = CHOP$(DEL$(config$, pos, "#")) SAVE config$ & NL$ TO Conf$ Load_Config(id) CALL GUIFN(id, "question_dialog", hide) CASE "err" CALL GUIFN(id, "err", hide) ENDSELECT WEND ENDSUB '----------------------------------------------------------------------------------------------- Main_Function(Create_Gui()) '-----------------------------------------------------------------------------------------------