'-----------------------------------------------------------------------------------------------
'
' Graphical Motif frontend for the 'mpg123' player. 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: added asynchronous title lookup using FORK()
' Version 1.2: exchanged buttons for popup, other tweaks for layout
' Version 1.3: tear-off menu, single click on list tunes station
' Version 1.4: code cleaning
' 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: more stability in network code
' Version 1.9: improved default font, code improvement
' Version 1.10: set timeout for mpg123
' Version 1.11: better title tracking
'
'-----------------------------------------------------------------------------------------------
' Install correct X11 font package for lucida-bold:
'
' # sudo apt install xfonts-75dpi
' # fc-cache
' # xset +fp /usr/share/fonts/X11/75dpi/
'-----------------------------------------------------------------------------------------------

OPTION GUI TRUE
PRAGMA GUI motif

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

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

SUB My_Update

	LOCAL config$, item$, url$, total$
	LOCAL x, count, pos, mynet, area, ch

	IF FILEEXISTS(Ipc$) THEN CALL GUISET(Current_Window, "playing", XmNlabelString, XmStringCreateLocalized(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 (*clr)() = XmListDeleteAllItems TYPE void
	LOCAL (*add)() = XmListAddItem TYPE void

	CALL GUIFN(id, "list", clr)

	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, XmStringCreateLocalized(TOKEN$(name$, 2, "=")), 0)
		NEXT
	ENDIF

ENDSUB

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

FUNCTION Create_Gui

	LOCAL window
	LOCAL (*invisible)() = XtUnmanageChild TYPE void

	window = GUIDEFINE( " \
	{ type=window name=window callback=window resources=\"*renderTable:*lucida-bold-r-*-*-14-140-*\" XmNtitle=\"BaCon Internet Radio v1.11\" XmNminWidth=550 XmNminHeight=300 } \
	{ type=xmFormWidgetClass name=mainform parent=window } \
	{ type=xmFrameWidgetClass name=frame1 parent=mainform XmNborderWidth=0 XmNleftAttachment=XmATTACH_FORM XmNtopAttachment=XmATTACH_FORM } \
	{ type=xmLabelWidgetClass name=label1 parent=frame1 XmNframeChildType=XmFRAME_TITLE_CHILD XmNchildVerticalAlignment=XmALIGNMENT_CENTER } \
	{ type=xmFormWidgetClass name=form1 parent=frame1 XmNborderWidth=0 } \
	{ type=xmScrolledWindowWidgetClass name=view parent=form1 XmNleftAttachment=XmATTACH_FORM XmNrightAttachment=XmATTACH_FORM XmNbottomAttachment=XmATTACH_FORM XmNtopAttachment=XmATTACH_FORM } \
	{ type=xmListWidgetClass name=list parent=view callback=XmNsingleSelectionCallback XmNselectionPolicy=XmSINGLE_SELECT } \
	{ type=xmFrameWidgetClass name=frame3 parent=mainform XmNborderWidth=0 XmNleftAttachment=XmATTACH_FORM XmNrightAttachment=XmATTACH_FORM XmNbottomAttachment=XmATTACH_FORM } \
	{ type=xmLabelWidgetClass name=label3 parent=frame3 XmNframeChildType=XmFRAME_TITLE_CHILD XmNchildVerticalAlignment=XmALIGNMENT_CENTER } \
	{ type=xmLabelWidgetClass name=playing parent=frame3 XmNalignment=XmALIGNMENT_BEGINNING } \
	{ type=sessionShellWidgetClass name=inputform parent=window } \
	{ type=xmSelectionBoxWidgetClass name=dialog parent=inputform XmNwidth=500 callback=XmNcancelCallback,cancel_entry callback=XmNokCallback,ok_entry XmNdialogType=XmDIALOG_PROMPT } \
	{ type=xmRowColumnWidgetClass name=form5 parent=dialog XmNborderWidth=0 } \
	{ type=xmLabelWidgetClass name=station parent=form5 XmNleftAttachment=XmATTACH_FORM XmNalignment=XmALIGNMENT_BEGINNING } \
	{ type=xmTextWidgetClass name=station_name parent=form5 XmNleftAttachment=XmATTACH_FORM XmNinput=1 } \
	{ type=xmMenuShellWidgetClass name=menu parent=list XmNwidth=1 XmNheight=1 XmNoverrideRedirect=True } \
	{ type=xmRowColumnWidgetClass name=menu_form parent=menu XmNmappedWhenManaged=False XmNrowColumnType=XmMENU_POPUP XmNpopupEnabled=XmPOPUP_AUTOMATIC XmNtearOffModel=XmTEAR_OFF_ENABLED } \
	{ type=xmPushButtonWidgetClass name=Pause parent=menu_form callback=XmNactivateCallback XmNborderWidth=0 } \
	{ type=xmPushButtonWidgetClass name=Resume parent=menu_form callback=XmNactivateCallback XmNborderWidth=0 } \
	{ type=xmSeparatorWidgetClass name=sep1 parent=menu_form } \
	{ type=xmPushButtonWidgetClass name=Add parent=menu_form callback=XmNactivateCallback XmNborderWidth=0 } \
	{ type=xmPushButtonWidgetClass name=Edit parent=menu_form callback=XmNactivateCallback XmNborderWidth=0 } \
	{ type=xmPushButtonWidgetClass name=Delete parent=menu_form callback=XmNactivateCallback XmNborderWidth=0 } \
	{ type=xmSeparatorWidgetClass name=sep2 parent=menu_form } \
	{ type=xmPushButtonWidgetClass name=Exit parent=menu_form callback=XmNactivateCallback XmNborderWidth=0 } \
	{ type=sessionShellWidgetClass name=confirm parent=window } \
	{ type=xmMessageBoxWidgetClass name=confirmdlg parent=confirm callback=XmNcancelCallback,cancel_station callback=XmNokCallback,ok_station XmNdialogType=XmDIALOG_QUESTION } \
	{ type=sessionShellWidgetClass name=err1 parent=window } \
	{ type=xmMessageBoxWidgetClass name=errdlg1 parent=err1 callback=XmNokCallback,ok_err1 XmNdialogType=XmDIALOG_WARNING }")

	CALL GUIFN(window, "menu_form", invisible)
	CALL GUIFN(GUIWIDGET(window, "dialog"), "Help", invisible)
	CALL GUIFN(GUIWIDGET(window, "confirmdlg"), "Help", invisible)
	CALL GUIFN(GUIWIDGET(window, "errdlg1"), "Help", invisible)
	CALL GUIFN(GUIWIDGET(window, "errdlg1"), "Cancel", invisible)

	CALL GUISET(window, "label1", XmNlabelString, XmStringCreateLocalized("Radio stations"))
	CALL GUISET(window, "label3", XmNlabelString, XmStringCreateLocalized("Currently playing"))
	CALL GUISET(window, "frame1", XmNrightAttachment, XmATTACH_WIDGET, XmNrightWidget, GUIWIDGET(window, "frame2"))
	CALL GUISET(window, "frame1", XmNbottomAttachment, XmATTACH_WIDGET, XmNbottomWidget, GUIWIDGET(window, "frame3"))
	CALL GUISET(window, "dialog", XmNdialogTitle, XmStringCreateLocalized("Radio Station"))
	CALL GUISET(window, "station", XmNlabelString, XmStringCreateLocalized("Radio Station Name:"))
	CALL GUISET(window, "Selection", XmNlabelString, XmStringCreateLocalized("Radio Station URL:"))
	CALL GUISET(window, "confirmdlg", XmNdialogTitle, XmStringCreateLocalized("Delete Station"))
	CALL GUISET(window, "confirmdlg", XmNmessageString, XmStringCreateLocalized("Are you sure?"))
	CALL GUISET(window, "confirmdlg", XmNcancelLabelString, XmStringCreateLocalized("No"))
	CALL GUISET(window, "confirmdlg", XmNokLabelString, XmStringCreateLocalized("Yes"))
	CALL GUISET(window, "errdlg1", XmNdialogTitle, XmStringCreateLocalized("Warning"))
	CALL GUISET(window, "errdlg1", XmNmessageString, XmStringCreateLocalized("No 'mpg123' found on this system!"))
	CALL GUISET(window, "playing", XmNlabelString, XmStringCreateLocalized("Use right mouse button for menu..."))
	CALL GUISET(window, "Add", XtVaTypedArg, XmNbackground, XtRString, "grey75", 7)
	CALL GUISET(window, "Edit", XtVaTypedArg, XmNbackground, XtRString, "grey75", 7)
	CALL GUISET(window, "Delete", XtVaTypedArg, XmNbackground, XtRString, "grey75", 7)
	CALL GUISET(window, "Resume", XtVaTypedArg, XmNbackground, XtRString, "grey75", 7)
	CALL GUISET(window, "Pause", XtVaTypedArg, XmNbackground, XtRString, "grey75", 7)
	CALL GUISET(window, "Exit", XtVaTypedArg, XmNbackground, XtRString, "grey75", 7)

	Load_Config$(window)
	Current_Window = window

	RETURN window

ENDFUNCTION

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

SUB Main_Function(id)

	LOCAL name$, url$, status$, config$, item$
	LOCAL name, url TYPE char*
	LOCAL count
	LOCAL pos TYPE int*
	LOCAL (*hide)() = XtPopdown TYPE void
	LOCAL (*show)() = XtPopup TYPE void
	LOCAL (*focus)() = XmProcessTraversal TYPE Boolean

	IF FILEEXISTS(Ipc$) THEN DELETE FILE Ipc$
	CALL My_Update()

	WHILE TRUE
		SELECT GUIEVENT$(id)
			CASE "Add"
				CALL GUIFN(id, "inputform", show, XtGrabNonexclusive)
				CALL GUIFN(id, "station_name", focus, XmTRAVERSE_CURRENT)
				CALL GUISET(id, "station_name", XmNvalue, "")
				CALL GUISET(id, "Text", XmNvalue, "")
				status$ = "added"
			CASE "Edit"
				CALL GUIFN(id, "inputform", show, XtGrabNonexclusive)
				CALL GUIFN(id, "station_name", focus, XmTRAVERSE_CURRENT)
				CALL GUIGET(id, "list", XmNselectedPositionCount, &count)
				IF count THEN
					CALL GUIGET(id, "list", XmNselectedPositions, &pos)
					config$ = LOAD$(Conf$)
					item$ = CHOP$(TOKEN$(config$, pos[0], "#"))
					name$ = TOKEN$(item$, 1, NL$)
					url$ = TOKEN$(item$, 2, NL$)
					CALL GUISET(id, "station_name", XmNvalue, TOKEN$(name$, 2, "="))
					CALL GUISET(id, "Text", XmNvalue, TOKEN$(url$, 2, "="))
					status$ = "edited"
				ENDIF
			CASE "Delete"
				CALL GUIGET(id, "list", XmNselectedPositionCount, &count)
				IF count THEN CALL GUIFN(id, "confirm", show, XtGrabNonexclusive)
			CASE "Exit", "window"
				IF FILEEXISTS(Fifo$) THEN APPEND "QUIT" & NL$ TO Fifo$
				IF FILEEXISTS(Ipc$) THEN DELETE FILE Ipc$
				END
			CASE "ok_entry"
				config$ = LOAD$(Conf$)
				IF status$ = "edited" THEN
					CALL GUIGET(id, "list", XmNselectedPositions, &pos)
					config$ = CHOP$(DEL$(config$, pos[0], "#"))
				ENDIF
				CALL GUIGET(id, "station_name", XmNvalue, &name)
				CALL GUIGET(id, "Text", XmNvalue, &url)
				config$ = CHOP$(config$) & NL$ & "name=" & name & NL$ & "url=" & url & NL$ & "#" & NL$
				SAVE SORT$(config$, "#" & NL$) & "#" & NL$ TO Conf$
				CALL GUIFN(id, "inputform", hide)
				Load_Config$(id)
			CASE "cancel_entry"
				CALL GUIFN(id, "inputform", hide)
			CASE "ok_station"
				CALL GUIGET(id, "list", XmNselectedPositions, &pos)
				config$ = LOAD$(Conf$)
				config$ = CHOP$(DEL$(config$, pos[0], "#"))
				SAVE config$ & NL$ TO Conf$
				CALL GUIFN(id, "confirm", hide)
				Load_Config$(id)
			CASE "cancel_station"
				CALL GUIFN(id, "confirm", hide)
			CASE "list", "Resume"
				IF NOT(LEN(EXEC$("command -v mpg123 2>/dev/null"))) THEN
					CALL GUIFN(id, "err1", show, XtGrabNonexclusive)
				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$)
					CALL GUIGET(id, "list", XmNselectedPositionCount, &count)
					IF count THEN
						CALL GUIGET(id, "list", XmNselectedPositions, &pos)
						config$ = LOAD$(Conf$)
						item$ = CHOP$(TOKEN$(config$, pos[0], "#"))
						url$ = TOKEN$(item$, 2, NL$)
						APPEND "LOAD " & TOKEN$(url$, 2, "=") & NL$ TO Fifo$
						Current_Url$ = url$
						Current_Station$ = TOKEN$(item$, 1, NL$)
						CALL GUISET(Current_Window, "playing", XmNlabelString, XmStringCreateLocalized("Fetching track info..."))
					ENDIF
				ENDIF
			CASE "Pause"
				IF FILEEXISTS(Fifo$) THEN APPEND "QUIT" & NL$ TO Fifo$
			CASE "ok_err1"
				CALL GUIFN(id, "err1", hide)
		ENDSELECT
	WEND

ENDSUB

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

Main_Function(Create_Gui())

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