'-----------------------------------------------------------------------------------------------
'
' 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())

'-----------------------------------------------------------------------------------------------
