message:
Optimized MRSS thumbnailing to improve performance

--HG--
extra : convert_revision : svn%3A1a989651-ce6e-ed51-1896-c0ee68ac2431/trunk%4017

repos: cbojar/jb-roku,cdgeeko/jb-roku

license: mit

diff:
--- source/lib/NWM_MRSS.brs
+++ source/lib/NWM_MRSS.brs
@@ -31,10 +31,25 @@
 	else
 		raw = ReadASCIIFile(m.url)
 	end if
-	'print raw

 	xml = CreateObject("roXMLElement")
 	if xml.Parse(raw)
+		' Channel thumbnail
+		channelthumb = ""
+		if xml.channel.image.url.Count() > 0 ' rss standard image tag
+			channelthumb = ValidStr(xml.channel.image.url.GetText())
+		else
+			tmp = xml.channel.GetNamedElements("media:thumbnail") ' mrss media tag
+			if tmp.Count() > 0
+				channelthumb = ValidStr(tmp[0]@url)
+			else
+				tmp = xml.channel.GetNamedElements("itunes:image") ' less-than-standard itunes image tag
+				if tmp.Count() > 0
+					channelthumb = ValidStr(tmp[0]@href)
+				end if
+			end if
+		end if
+
 		for each item in xml.channel.item
 			newItem = {
 				streams:	[]
@@ -73,23 +88,9 @@
 				newItem.hdPosterURL_hq = ValidStr(tmp[0]@url)
 			end if

-			' thumbnail
-			if xml.channel.image.url.Count() > 0 ' rss standard image tag
-				newItem.sdPosterURL = ValidStr(xml.channel.image.url.GetText())
-				newItem.hdPosterURL = ValidStr(xml.channel.image.url.GetText())
-			else
-				tmp = xml.channel.GetNamedElements("media:thumbnail") ' mrss media tag
-				if tmp.Count() > 0
-					newItem.sdPosterURL = ValidStr(tmp[0]@url)
-					newItem.hdPosterURL = ValidStr(tmp[0]@url)
-				else
-					tmp = xml.channel.GetNamedElements("itunes:image") ' less-than-standard itunes image tag
-					if tmp.Count() > 0
-						newItem.sdPosterURL = ValidStr(tmp[0]@href)
-						newItem.hdPosterURL = ValidStr(tmp[0]@href)
-					end if
-				end if
-			end if
+			' thumbnail (from channel)
+			newItem.sdPosterURL = ValidStr(channelthumb)
+			newItem.hdPosterURL = ValidStr(channelthumb)

 			' categories
 			tmp = item.GetNamedElements("media:category")


subject: Update stream url

message:
Update stream url

Use the new stream url

diff:
--- source/Main.brs
+++ source/Main.brs
@@ -111,7 +111,7 @@
 		sdPosterURL:		"pkg:/images/mm_icon_focus_sd.png"
 		hdPosterURL:		"pkg:/images/mm_icon_focus_hd.png"
 		contentType:		"episode"
-		streamurls:		["http://videocdn-us.geocdn.scaleengine.net/jblive-iphone/live/jblive.stream/playlist.m3u8"]
+		streamurls:		["http://jblive.videocdn.scaleengine.net/jb-live/play/jblive.stream/playlist.m3u8"]
 		streamformat:		"hls"
 		streamqualities:	["SD"]
 		streambitrates:	[0]


subject: Removed deliberately broken test. Travis CI should be fully working now.

repos: imbenjamin/brsHamcrest


message:
Fixed crash for associative arrays that contain properties with n… (#49)

Co-authored-by: Gareth Jones <a88b7dcd1a9e3e17770bbaa6d7515b31a2d7e85d@gareth.co.uk>

diff:
--- source/brsHamcrest_Matchers/brsHamcrest_CoreMatchers.brs
+++ source/brsHamcrest_Matchers/brsHamcrest_CoreMatchers.brs
@@ -191,7 +191,7 @@
                         return false
                     end if
                 else if (targetType = "roAssociativeArray")
-                    if (target.Count() = m.comparison.Count())
+                    if (m._getAssocArrayPropertyCount(target) = m._getAssocArrayPropertyCount(m.comparison))
                         for each key in target
                             if (equalTo(m.comparison.LookupCI(key)).doMatch(target.LookupCI(key)) = false) return false
                         end for
@@ -213,6 +213,17 @@
             end if

             return true
+        end function
+
+        ' a limitation of BS is that an object can sometimes contain properties with names that happen to match the getters that are used
+        ' to deduce the size of those objects (i.e. count, items, keys), and BS can't tell them apart which causes an exception.
+        ' so here we avoid that issue by brute-force manually counting each property instead.
+        _getAssocArrayPropertyCount: function (obj as Object) as Integer
+            i = 0
+            for each key in obj
+                i++
+            end for
+            return i
         end function

         _normaliseType: function (typeIn as String) as String


subject: EE-633 Live only shows play, other screens have resume and start from beginning

repos: NewSpring/Public-Roku

diff:
--- apps/source/newspring-public/source/appDetailScreen.brs
+++ apps/source/newspring-public/source/appDetailScreen.brs
@@ -118,7 +118,17 @@
     'PrintAA(show)

     screen.ClearButtons()
-    screen.AddButton(1, "Play")
+
+    if showList[showIndex].LiveStream = "true" then
+      screen.AddButton(1, "Play")
+    else
+      if regread(show.contentid) <> invalid and regread(show.contentid).toint() >=30 then
+        screen.AddButton(1, "Resume playing")
+        screen.AddButton(2, "Play from beginning")
+      else
+        screen.addbutton(2,"Play")
+      end if
+    endif

     screen.SetContent(show)


subject: Convert SpringboardScreen to use WatchedStatusTracker.

repos: cdgeeko/jb-roku,cbojar/jb-roku

diff:
--- source/screens/SpringboardScreen.brs
+++ source/screens/SpringboardScreen.brs
@@ -4,6 +4,7 @@
 	instance.screen = CreateObject("roSpringboardScreen")
 	instance.messagePort = CreateObject("roMessagePort")
 	instance.screen.SetMessagePort(instance.messagePort)
+	instance.watchedStatusTracker = WatchedStatusTracker()
 	instance.lastMessage = invalid
 	instance.screen.SetStaticRatingEnabled(false)

@@ -66,7 +67,7 @@

 	instance.isPartiallyWatched = function()
 		episode = m.getSelectedEpisode()
-		return RegRead(episode.title) <> invalid and RegRead(episode.title).ToInt() >= 30
+		return m.watchedStatusTracker.hasProgress(episode.title)
 	end function

 	instance.waitForInput = function()
@@ -97,11 +98,12 @@
 		msg = m.getLastMessage()
 		episode = m.getSelectedEpisode()
 		if msg.GetIndex() = 1
-			PlayStart = RegRead(episode.title)
-			if PlayStart <> invalid then
-				episode.PlayStart = PlayStart.ToInt()
+			PlayStart = m.watchedStatusTracker.getProgress(episode.title)
+			if PlayStart > 0 then
+				episode.PlayStart = PlayStart
 			end if
 		else if msg.GetIndex() = 2
+			m.watchedStatusTracker.removeProgress(episode.title)
 			episode.PlayStart = 0
 		end if
 		ShowVideoScreen(episode)


subject: Added information for live audio stream.

diff:
--- source/Main.brs
+++ source/Main.brs
@@ -117,7 +117,22 @@
 		streamurls:		["http://videocdn-us.geocdn.scaleengine.net/jblive-iphone/live/jblive.stream/playlist.m3u8"]
 		streamformat:		"hls"
 		streamqualities:	["SD"]
-		streambitrates:		[0]
+		streambitrates:	[0]
+		categories:		[]
+	}
+
+	result.audio = {
+		live:			true
+		title:			"Audio Stream"
+		shortDescriptionLine1:	"Audio Stream"
+		shortDescriptionLine2:	"Listen To JB Radio Live!"
+		sdPosterURL:		"pkg:/images/mm_icon_focus_sd.jpg"
+		hdPosterURL:		"pkg:/images/mm_icon_focus_hd.jpg"
+		contentType:		"episode"
+		streamurls:		["http://jbradio.out.airtime.pro:8000/jbradio_a"]
+		streamformat:		"mp3"
+		streambitrates:	[0]
+		streamqualities:	["SD"]
 		categories:		[]
 	}


subject: Encapsulate show functionality, and fix problem where navigating between episodes via directional buttons did not properly refresh everything.

diff:
--- source/screens/SpringboardScreen.brs
+++ source/screens/SpringboardScreen.brs
@@ -6,13 +6,7 @@
 	'screen.setAdSelectable( false )

 	screen.SetStaticRatingEnabled( false )
-	setButtons( screen, episodes[selectedEpisode] )
-	screen.Show()
-
-	SpringBoardScreen_swapPoster( episodes[selectedEpisode] )
-	screen.SetContent( episodes[selectedEpisode] )
-	screen.Show()
-	SpringBoardScreen_swapPoster( episodes[selectedEpisode] )
+	SpringBoardScreen_showContent( screen, episodes[selectedEpisode] )

 	while true
 		msg = wait( 0, screen.GetMessagePort() )
@@ -42,20 +36,30 @@
 					else
 						selectedEpisode = selectedEpisode - 1
 					end if
-					screen.SetContent( episodes[selectedEpisode] )
+					SpringBoardScreen_showContent( screen, episodes[selectedEpisode] )
 				else if msg.GetIndex() = 5 ' RIGHT
 					if selectedEpisode = episodes.Count() - 1
 						selectedEpisode = 0
 					else
 						selectedEpisode = selectedEpisode + 1
 					end if
-					screen.SetContent( episodes[selectedEpisode] )
+                    SpringBoardScreen_showContent( screen, episodes[selectedEpisode] )
 				end if
 			end if
 		end if
 	end while
 	return selectedEpisode
 end function
+
+sub SpringBoardScreen_showContent( screen, episode )
+    SpringBoardScreen_setButtons( screen, episode )
+    screen.Show()
+
+    SpringBoardScreen_swapPoster( episode )
+    screen.SetContent( episode )
+    screen.Show()
+    SpringBoardScreen_swapPoster( episode )
+end sub

 sub SpringBoardScreen_swapPoster( show )
 	tmp = show.sdPosterURL
@@ -70,7 +74,7 @@
 	end if
 end sub

-sub setButtons( screen, episode )
+sub SpringBoardScreen_setButtons( screen, episode )
 	screen.ClearButtons()
 	if RegRead( episode.title ) <> invalid and RegRead( episode.title ).toint() >=30 then
 		screen.AddButton( 1, "Resume Playing" )


subject: Added audio live stream to live stream screen.

diff:
--- source/screens/LivestreamScreen.brs
+++ source/screens/LivestreamScreen.brs
@@ -8,7 +8,7 @@
 	' Disable star ratings
 	screen.SetStaticRatingEnabled( false )
 	' Add "Play" button
-	screen.AddButton( 1, "Play" )
+	Livestream_SetButtons( screen, false )
 	' Set content to live stream (automatically parses array)
 	screen.SetContent( livestream )
 	' Show screen
@@ -99,17 +99,39 @@
 		screen.SetContent( livestream )
 		screen.Show()
 	end if
+
+	AudioStream = CreateAudioPlayer( screen.GetMessagePort() )
+	AudioStream.SetStream( livestream.audio )

 	while true
 		msg = wait( 0, screen.GetMessagePort() )
 		if msg <> invalid
 			if msg.isScreenClosed()
+				AudioStream.Stop()
 				exit while
 			else if msg.isButtonPressed()
-				ShowVideoScreen( livestream )
+				if msg.GetIndex() = 1
+					AudioStream.Stop()
+					ShowVideoScreen( livestream )
+				else if msg.GetIndex() = 2
+					AudioStream.Play()
+				else if msg.GetIndex() = 3
+					AudioStream.Stop()
+				endif
+				Livestream_SetButtons( screen, AudioStream.IsPlaying() )
 			endif
 		end if
 	end while

 	return selectedEpisode
 end function
+
+sub Livestream_SetButtons( screen, isPlaying )
+	screen.ClearButtons()
+	if isPlaying
+		screen.AddButton( 3, "Stop Listening" )
+	else
+		screen.AddButton( 1, "Play" )
+		screen.AddButton( 2, "Listen" )
+	end if
+end sub


subject: Clean up VideoScreen a bit, and increase interval between progress save marker.

diff:
--- source/screens/VideoScreen.brs
+++ source/screens/VideoScreen.brs
@@ -24,16 +24,12 @@
 	screen.SetMessagePort(port)

 	screen.SetContent( episode )
-	screen.SetPositionNotificationPeriod( 3 ) 'Fire isPlaybackPosition every 3 seconds
+	screen.SetPositionNotificationPeriod( 15 )
 	screen.Show()
-
-	'Uncomment his line to dump the contents of the episode to be played
-	'PrintAA(episode)

 	while true
 		msg = wait(0, port)
 		if type(msg) = "roVideoScreenEvent" then
-			print "showHomeScreen | msg = "; msg.getMessage() " | index = "; msg.GetIndex()
 			if msg.isScreenClosed()
 				print "Screen closed"
 				exit while
@@ -44,11 +40,11 @@
 			else if msg.isButtonPressed()
 				print "Button pressed: "; msg.GetIndex(); " " msg.GetData()
 			else if msg.isPlaybackPosition() then
-		                nowpos = msg.GetIndex()
-		                RegWrite( episode.title, nowpos.toStr() )
+				nowpos = msg.GetIndex()
+				RegWrite( episode.title, nowpos.toStr() )
 				print "Marked progress: "; msg.GetIndex()
 			else if msg.isFullResult() then
-		                RegDelete( episode.title )
+				RegDelete( episode.title )
 			else
 				print "Unexpected event type: "; msg.GetType()
 			end if


subject: Function setButtons has been renamed.

message: Function setButtons has been renamed.

diff:
--- source/screens/SpringboardScreen.brs
+++ source/screens/SpringboardScreen.brs
@@ -14,19 +14,19 @@
 			if msg.isScreenClosed()
 				exit while
 			else if msg.isButtonPressed()
-	                	if msg.GetIndex() = 1
+				if msg.GetIndex() = 1
 					PlayStart = RegRead( episodes[selectedEpisode].title )
 					if PlayStart <> invalid then
 						episodes[selectedEpisode].PlayStart = PlayStart.ToInt()
 					end if
 					ShowVideoScreen( episodes[selectedEpisode] )
-					setButtons( screen, episodes[selectedEpisode] )
+					SpringBoardScreen_setButtons( screen, episodes[selectedEpisode] )
 					screen.Show()
 				end if
 				if msg.GetIndex() = 2
 					episodes[selectedEpisode].PlayStart = 0
 					ShowVideoScreen( episodes[selectedEpisode] )
-					setButtons( screen, episodes[selectedEpisode] )
+					SpringBoardScreen_setButtons( screen, episodes[selectedEpisode] )
 					screen.Show()
 				end if
 			else if msg.isRemoteKeyPressed()


subject: All files must end with a newline for concatenation (again).

diff:
--- source/lib/AudioPlayer.brs
+++ source/lib/AudioPlayer.brs
@@ -28,4 +28,4 @@
 Function AudioPlayer_SetStream( audioStream As Object )
 	m.player.AddContent( audioStream )
 	m.player.SetLoop( false )
-end Function+end Function


subject: Set live stream quality to SD for compatibility with SD users. This also appears to work the same for HD users as before.

diff:
--- source/Main.brs
+++ source/Main.brs
@@ -116,7 +116,7 @@
 		contentType:		"episode"
 		streamurls:		["http://videocdn-us.geocdn.scaleengine.net/jblive-iphone/live/jblive.stream/playlist.m3u8"]
 		streamformat:		"hls"
-		streamqualities:	["HD"]
+		streamqualities:	["SD"]
 		streambitrates:		[0]
 		categories:		[]
 	}


subject: Check for upcoming events to prevent crash. Fixes #5.

diff:
--- source/screens/LivestreamScreen.brs
+++ source/screens/LivestreamScreen.brs
@@ -69,6 +69,10 @@
 	instance.getDescriptionWithCalendar = function(calendar)
 		if calendar = invalid
 			return "Calendar could not be loaded."
+		end if
+
+		if calendar.getChildElements() = invalid
+			return "No upcoming events found."
 		end if

 		nextEvent = m.getNextEvent(calendar)


subject: bug 1109918 - use generic description style on detail screen

diff:
--- roku/airmozilla/source/appDetailScreen.brs
+++ roku/airmozilla/source/appDetailScreen.brs
@@ -5,7 +5,8 @@
 Function preShowDetailScreen(breadA=invalid, breadB=invalid) As Object
     port=CreateObject("roMessagePort")
     screen = CreateObject("roSpringboardScreen")
-    screen.SetDescriptionStyle("video")
+    ' screen.SetDescriptionStyle("video")
+    screen.SetDescriptionStyle("generic")
     screen.SetMessagePort(port)
     if breadA<>invalid and breadB<>invalid then
         screen.SetBreadcrumbText(breadA, breadB)


message:
Added/copy-pasted additional library functionality.

--HG--
extra : convert_revision : svn%3A1a989651-ce6e-ed51-1896-c0ee68ac2431/trunk%4013

diff:
--- source/lib/General.brs
+++ source/lib/General.brs
@@ -37,3 +37,33 @@
     return false
 End Function

+'******************************************************
+'Return a newline character
+'******************************************************
+Function nl()
+    return chr(10)
+End Function
+
+'******************************************************
+'Registry Helper Functions
+'******************************************************
+Function RegRead(key, section=invalid)
+    if section = invalid then section = "Default"
+    sec = CreateObject("roRegistrySection", section)
+    if sec.Exists(key) then return sec.Read(key)
+    return invalid
+End Function
+
+Function RegWrite(key, val, section=invalid)
+    if section = invalid then section = "Default"
+    sec = CreateObject("roRegistrySection", section)
+    sec.Write(key, val)
+    sec.Flush() 'commit it
+End Function
+
+Function RegDelete(key, section=invalid)
+    if section = invalid then section = "Default"
+    sec = CreateObject("roRegistrySection", section)
+    sec.Delete(key)
+    sec.Flush()
+End Function


subject: updated ip

message: updated ip

diff:
--- apps/source/live/source/categoryFeed.brs
+++ apps/source/live/source/categoryFeed.brs
@@ -12,7 +12,8 @@

     conn = CreateObject("roAssociativeArray")

-    conn.UrlPrefix   = "http://10.0.200.114/roku"
+    conn.UrlPrefix   = "http://10.0.20.150/roku"
+
     conn.UrlCategoryFeed = conn.UrlPrefix + "/categories"

     conn.Timer = CreateObject("roTimespan")


subject: EE-559 Changed category feed to production url

diff:
--- apps/source/live/source/categoryFeed.brs
+++ apps/source/live/source/categoryFeed.brs
@@ -12,7 +12,7 @@

     conn = CreateObject("roAssociativeArray")

-    conn.UrlPrefix   = "http://10.0.20.150/roku"
+    conn.UrlPrefix   = "https://newspring.cc/roku"

     conn.UrlCategoryFeed = conn.UrlPrefix + "/categories"


subject: Disabled beta quality feature.

message:
Disabled beta quality feature.

--HG--
extra : convert_revision : svn%3A1a989651-ce6e-ed51-1896-c0ee68ac2431/trunk%4027

diff:
--- source/Main.brs
+++ source/Main.brs
@@ -67,7 +67,7 @@
 		end if
 	end if

-	result.push( GetQualityIcon() )
+	'result.push( GetQualityIcon() )

 	return result
 end function


subject: Replaced Play with Watch on livestream screen to remove ambiguity.

diff:
--- source/screens/LivestreamScreen.brs
+++ source/screens/LivestreamScreen.brs
@@ -131,7 +131,7 @@
 	if isPlaying
 		screen.AddButton( 3, "Stop Listening" )
 	else
-		screen.AddButton( 1, "Play" )
+		screen.AddButton( 1, "Watch" )
 		screen.AddButton( 2, "Listen" )
 	end if
 end sub


subject: Some comments added in roku

message: Some comments added in roku

diff:
--- roku/main.brs
+++ roku/main.brs
@@ -1,3 +1,16 @@
+'To get the authentication token for Junction TV API calls
+
+'For detailed description read: http://api.junctiontv.com/jtv/jtapi/getting-access-tokens-2
+'Refer Github: https://github.com/JunctionTV/Getting-Access-Tokens
+
+'-----------------------------------------------------------------------------------------
+' Author: Subhankar Ganguly <subhankar1981@gmail.com>
+'
+' License: The MIT License (MIT)
+' Copyright (c) <2016> <Junction TV Inc.>
+'------------------------------------------------------------------------------------------
+
+
 Sub Main(args as Dynamic)
 print "Main called "
 respauth = ApiAuthentication()


message:
Enable gzip compression encoding for HTTP requests.

--HG--
extra : convert_revision : svn%3A1a989651-ce6e-ed51-1896-c0ee68ac2431/trunk%4011

diff:
--- source/lib/NWM_Utilities.brs
+++ source/lib/NWM_Utilities.brs
@@ -20,6 +20,7 @@

   ut = CreateObject("roURLTransfer")
   ut.SetPort(CreateObject("roMessagePort"))
+  ut.EnableEncodings(true) '' Enable gzip encoding
   if userAgent <> ""
 	  ut.AddHeader("user-agent", userAgent)
 	end if


subject: Don't need to be so showy.

message: Don't need to be so showy.

diff:
--- source/screens/SpringboardScreen.brs
+++ source/screens/SpringboardScreen.brs
@@ -53,7 +53,6 @@

 sub SpringBoardScreen_showContent( screen, episode )
     SpringBoardScreen_setButtons( screen, episode )
-    screen.Show()

     SpringBoardScreen_swapPoster( episode )
     screen.SetContent( episode )


subject: Updating category feed to point to production

diff:
--- apps/source/kidspring-public/source/categoryFeed.brs
+++ apps/source/kidspring-public/source/categoryFeed.brs
@@ -13,7 +13,7 @@
     conn = CreateObject("roAssociativeArray")

     ' This also must be updated in analytics brs
-    conn.UrlPrefix   = "http://newspring.dev.10.0.200.164.xip.io/roku"
+    conn.UrlPrefix   = "http://newspring.cc/roku"

     conn.UrlCategoryFeed = conn.UrlPrefix + "/kidspring"


message:
Added 12hr clock to "Next Live Show" display. Commented calendar capture and display.

--HG--
extra : convert_revision : svn%3A1a989651-ce6e-ed51-1896-c0ee68ac2431/trunk%4018

diff:
--- source/screens/LivestreamScreen.brs
+++ source/screens/LivestreamScreen.brs
@@ -5,12 +5,18 @@
 	'screen.setAdURL( sdAd, hdAd )
 	'screen.setAdSelectable( false )

+	' Disable star ratings
 	screen.SetStaticRatingEnabled( false )
+	' Add "Play" button
 	screen.AddButton( 1, "Play" )
+	' Set content to live stream (automatically parses array)
 	screen.SetContent( livestream )
+	' Show screen
 	screen.Show()

+	' Fetch calendar feed
 	raw = NWM_UT_GetStringFromURL( livestream.calendar )
+	' Parse calendar feed
 	calendar = CreateObject( "roXMLElement" )
 	if calendar.Parse( raw )
 		islive = false
@@ -20,12 +26,16 @@
 		now_s = now.asSeconds()

 		for each event in calendar.Event
+			' Parse start and end times into ints
 			estart = strtoi( ValidStr( event.start.GetText() ) )
 			efinish = strtoi( ValidStr( event.finish.GetText() ) )
+			' If nextevent is not set, set to current event (first run)
 			if nextevent = invalid
 				nextevent = event
 			else
+				' Parse event's start time
 				nestart = strtoi( ValidStr( nextevent.start.GetText() ) )
+				' If event starts before nextevent and finishes after current time
 				if estart < nestart AND efinish > now_s
 					nextevent = event
 				end if
@@ -33,21 +43,53 @@
 		next
 		nestart = strtoi( ValidStr( nextevent.start.GetText() ) )
 		nefinish = strtoi( ValidStr( nextevent.finish.GetText() ) )
-		islive = ( nestart < now_s AND nefinish > now_s )
-		desc = strReplace( nextevent.summary.GetText(), "LIVE: ", "" )
+
+		islive = ( nestart < now_s AND nefinish > now_s ) ' If event starts before now and ends after now, it is live
+		desc = strReplace( nextevent.summary.GetText(), "LIVE: ", "" ) ' Remove "Live:" portions from description
 		if islive
 			desc = "LIVE NOW: " + desc
 		else
 			nextlive = CreateObject( "roDateTime" )
 			nextlive.fromSeconds( nestart )
 			nextlive.toLocalTime()
-			desc = "Next Live Show:" + nl() + desc + " ("
+
+			' Show next live show name and date
+			desc = "Next Live Show: " + desc + nl()
 			desc = desc + nextlive.asDateString( "long-date" ) + " "
+
+			' Show next live show time (24hr)
 			desc = desc + itostr( nextlive.getHours() ) + ":"
-			if nextlive.getMinutes() < 10
+			if nextlive.getMinutes() < 10 ' If less than 10 minutes, add leading zero
 				desc = desc + "0"
 			end if
-			desc = desc + itostr( nextlive.getMinutes() ) + ")"
+			desc = desc + itostr( nextlive.getMinutes() )
+
+			' Show next live show time (12hr)
+			ispm = nextlive.getHours() > 11 ' If hours is more than 11, is PM
+			desc = desc + " ("
+			if ispm ' If is PM, subtract 12 and show hours
+				if nextlive.getHours() = 12 ' If is PM and is 12, show 12
+					desc = desc + "12:"
+				else
+					desc = desc + itostr( nextlive.getHours() - 12 ) + ":"
+				end if
+			else ' If is AM show hours
+				if nextlive.getHours() = 0 ' If is AM and is 0, show 12
+					desc = desc + "12:"
+				else
+					desc = desc + itostr( nextlive.getHours() ) + ":"
+				end if
+			end if
+			if nextlive.getMinutes() < 10 ' If less than 10 minutes, add leading zero
+				desc = desc + "0"
+			end if
+			desc = desc + itostr( nextlive.getMinutes() ) + " "
+			if ispm ' If is PM, show PM
+				desc = desc + "PM"
+			else ' If is AM, show AM
+				desc = desc + "AM"
+			end if
+			desc = desc + ")"
 		end if

 		desc = desc + nl() + nl() + "Live pre-shows generally start approximately 15 minutes before shows."


subject: Added simple loading dialog.

message: Added simple loading dialog.

stderr: error: pathspec 'source/dialogs/LoadingDialog.brs' did not match any file(s) known to git

diff:
--- source/dialogs/LoadingDialog.brs
+++ source/dialogs/LoadingDialog.brs
@@ -0,0 +1,11 @@
+Function ShowLoadingDialog( title="", showBusy = false ) As Object
+	dialog = CreateObject( "roOneLineDialog" )
+
+	dialog.SetTitle( title )
+	if showBusy
+		dialog.ShowBusyAnimation()
+	end if
+	dialog.Show()
+
+	return dialog
+End Function


subject: Create WatchedStatusTracker to encapsulate tracking all watch progress.

stderr: error: pathspec 'source/lib/WatchedStatusTracker.brs' did not match any file(s) known to git

diff:
--- source/lib/WatchedStatusTracker.brs
+++ source/lib/WatchedStatusTracker.brs
@@ -0,0 +1,32 @@
+function WatchedStatusTracker()
+	instance = CreateObject("roAssociativeArray")
+
+	instance.saveProgress = function(title as String, currentPosition as Integer)
+		if(currentPosition < 30)
+			return false
+		end if
+
+		RegWrite(title, currentPosition.toStr())
+		return true
+	end function
+
+	instance.getProgress = function(title as String)
+		savedProgress = RegRead(title)
+		if savedProgress = invalid
+			return 0
+		end if
+
+		return savedProgress.toInt()
+	end function
+
+	instance.hasProgress = function(title as String)
+		savedProgress = RegRead(title)
+		return savedProgress <> invalid
+	end function
+
+	instance.removeProgress = function(title as String)
+		RegDelete(title)
+	end function
+
+	return instance
+end function


subject: updates roku SG app to allow theming of hud

stderr:
remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/oddnetworks/odd-sample-apps.git/'