Optimized MRSS thumbnailing to improve performance

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

''
''	NWM_MRSS
''	chagedorn@roku.com
''
''	A BrightScript class for parsing standard MRSS files
''	http://video.search.yahoo.com/mrss
''
''	Usage:
''		mrss = NWM_MRSS("http://www.example.com/mrss_feed.xml")	' iniitialize a NWM_MRSS object
''		episodes = mrss.GetEpisodes(10) ' get the first 10 episodes found in the MRSS feed
''		episodes = mrss.GetEpisodes() 	' get all episodes found in the MRSS feed
''

function NWM_MRSS(url)
	this = {
		url:	url

		GetEpisodes:	NWM_MRSS_GetEpisodes
	}

	return this
end function

' Build an array of content-meta-data objects suitable for passing to roPosterScreen::SetContentList()
function NWM_MRSS_GetEpisodes(limit = 0)
	result = []
	util = NWM_Utilities()

	if InStr(1, m.url, "http://") = 1
		raw = NWM_GetStringFromURL(m.url)
	else
		raw = ReadASCIIFile(m.url)
	end if

	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:	[]
				streamFormat:	"mp4"
				actors:		[]
				categories:	[]
				contentType:	"episode"
			}

			' title
			tmp = item.GetNamedElements("media:title")
			if tmp.Count() > 0
				newItem.title = util.HTMLEntityDecode(ValidStr(tmp[0].GetText()))
				newItem.shortDescriptionLine1 = util.HTMLEntityDecode(ValidStr(tmp[0].GetText()))
			else
				newItem.title = util.HTMLEntityDecode(ValidStr(item.title.GetText()))
				newItem.shortDescriptionLine1 = util.HTMLEntityDecode(ValidStr(item.title.GetText()))
			end if

			' description
			tmp = item.GetNamedElements("media:description")
			if tmp.Count() > 0
				description = util.HTMLEntityDecode(util.HTMLStripTags(ValidStr(tmp[0].GetText())))
				newItem.description = description
				newItem.synopsis = description
			else
				description = util.HTMLEntityDecode(util.HTMLStripTags(ValidStr(item.description.GetText())))
				newItem.description = description
				newItem.synopsis = description
			end if

			' hq thumbnail
			tmp = item.GetNamedElements("media:thumbnail")
			if tmp.Count() > 0
				newItem.sdPosterURL_hq = ValidStr(tmp[0]@url)
				newItem.hdPosterURL_hq = ValidStr(tmp[0]@url)
			end if

			' thumbnail (from channel)
			newItem.sdPosterURL = ValidStr(channelthumb)
			newItem.hdPosterURL = ValidStr(channelthumb)

			' categories
			tmp = item.GetNamedElements("media:category")
			if tmp.Count() > 0
				for each category in tmp
					newItem.categories.Push(ValidStr(category.GetText()))
				next
			end if

			' acrtors and director
			tmp = item.GetNamedElements("media:credit")
			if tmp.Count() > 0
				for each credit in tmp
					if ValidStr(credit@role) = "actor"
						newItem.actors.Push(ValidStr(credit.GetText()))
					else if ValidStr(credit@role) = "director"
						newItem.director = ValidStr(credit.GetText())
					end if
				next
			end if

			' rating
			tmp = item.GetNamedElements("media:rating")
			if tmp.Count() > 0
				newItem.rating = ValidStr(tmp[0].GetText())
			end if

			' release date
			newItem.releaseDate = ValidStr(item.pubdate.GetText())

			' media:content can be a child of <item> or of <media:group>
			contentItems = item.GetNamedElements("media:content")
			if contentItems.Count() = 0
				tmp = item.GetNamedElements("media:group")
				if tmp.Count() > 0
					contentItems = tmp.GetNamedElements("media:content")
				end if
			end if

			if contentItems.Count() > 0
				for each content in contentItems
					if ValidStr(content@url) <> ""
						newStream = {
							url:			ValidStr(content@url)
							bitrate:	StrToI(ValidStr(content@bitrate))
						}

						' use the content's height attribute to determine HD-ness
						if StrToI(ValidStr(content@height)) > 720
							newStream.quality = true
							newItem.HDBranded = true
							newItem.isHD = true
							newItem.fullHD = true
						else if StrToI(ValidStr(content@height)) > 480
							newStream.quality = true
							newItem.HDBranded = true
							newItem.isHD = true
						end if

						newItem.streams.push(newStream)
					end if
				next

				length = StrToI(ValidStr(contentItems[0]@duration))
				if newItem.length = invalid and length > 0
					newItem.length = length
				end if

				'PrintAA(newItem)
				result.Push(newItem)
			else if item.enclosure.Count() > 0
				' we didn't find any media:content tags, try the enclosure tag
				newStream = {
					url:	ValidStr(item.enclosure@url)
				}

				newItem.streams.Push(newStream)

				'PrintAA(newItem)
				result.Push(newItem)
			end if
		next
	end if

	return result
end function

''
''	NWM_MRSS
''	chagedorn@roku.com
''
''	A BrightScript class for parsing standard MRSS files
''	http://video.search.yahoo.com/mrss
''
''	Usage:
''		mrss = NWM_MRSS("http://www.example.com/mrss_feed.xml")	' iniitialize a NWM_MRSS object
''		episodes = mrss.GetEpisodes(10) ' get the first 10 episodes found in the MRSS feed
''		episodes = mrss.GetEpisodes() 	' get all episodes found in the MRSS feed
''

function NWM_MRSS(url)
	this = {
		url:	url

		GetEpisodes:	NWM_MRSS_GetEpisodes
	}

	return this
end function

' Build an array of content-meta-data objects suitable for passing to roPosterScreen::SetContentList()
function NWM_MRSS_GetEpisodes(limit = 0)
	result = []
	util = NWM_Utilities()

	if InStr(1, m.url, "http://") = 1
		raw = NWM_GetStringFromURL(m.url)
	else
		raw = ReadASCIIFile(m.url)
	end if
	'print raw

	xml = CreateObject("roXMLElement")
	if xml.Parse(raw)
		for each item in xml.channel.item
			newItem = {
				streams:	[]
				streamFormat:	"mp4"
				actors:		[]
				categories:	[]
				contentType:	"episode"
			}

			' title
			tmp = item.GetNamedElements("media:title")
			if tmp.Count() > 0
				newItem.title = util.HTMLEntityDecode(ValidStr(tmp[0].GetText()))
				newItem.shortDescriptionLine1 = util.HTMLEntityDecode(ValidStr(tmp[0].GetText()))
			else
				newItem.title = util.HTMLEntityDecode(ValidStr(item.title.GetText()))
				newItem.shortDescriptionLine1 = util.HTMLEntityDecode(ValidStr(item.title.GetText()))
			end if

			' description
			tmp = item.GetNamedElements("media:description")
			if tmp.Count() > 0
				description = util.HTMLEntityDecode(util.HTMLStripTags(ValidStr(tmp[0].GetText())))
				newItem.description = description
				newItem.synopsis = description
			else
				description = util.HTMLEntityDecode(util.HTMLStripTags(ValidStr(item.description.GetText())))
				newItem.description = description
				newItem.synopsis = description
			end if

			' hq thumbnail
			tmp = item.GetNamedElements("media:thumbnail")
			if tmp.Count() > 0
				newItem.sdPosterURL_hq = ValidStr(tmp[0]@url)
				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

			' categories
			tmp = item.GetNamedElements("media:category")
			if tmp.Count() > 0
				for each category in tmp
					newItem.categories.Push(ValidStr(category.GetText()))
				next
			end if

			' acrtors and director
			tmp = item.GetNamedElements("media:credit")
			if tmp.Count() > 0
				for each credit in tmp
					if ValidStr(credit@role) = "actor"
						newItem.actors.Push(ValidStr(credit.GetText()))
					else if ValidStr(credit@role) = "director"
						newItem.director = ValidStr(credit.GetText())
					end if
				next
			end if

			' rating
			tmp = item.GetNamedElements("media:rating")
			if tmp.Count() > 0
				newItem.rating = ValidStr(tmp[0].GetText())
			end if

			' release date
			newItem.releaseDate = ValidStr(item.pubdate.GetText())

			' media:content can be a child of <item> or of <media:group>
			contentItems = item.GetNamedElements("media:content")
			if contentItems.Count() = 0
				tmp = item.GetNamedElements("media:group")
				if tmp.Count() > 0
					contentItems = tmp.GetNamedElements("media:content")
				end if
			end if

			if contentItems.Count() > 0
				for each content in contentItems
					if ValidStr(content@url) <> ""
						newStream = {
							url:			ValidStr(content@url)
							bitrate:	StrToI(ValidStr(content@bitrate))
						}

						' use the content's height attribute to determine HD-ness
						if StrToI(ValidStr(content@height)) > 720
							newStream.quality = true
							newItem.HDBranded = true
							newItem.isHD = true
							newItem.fullHD = true
						else if StrToI(ValidStr(content@height)) > 480
							newStream.quality = true
							newItem.HDBranded = true
							newItem.isHD = true
						end if

						newItem.streams.push(newStream)
					end if
				next

				length = StrToI(ValidStr(contentItems[0]@duration))
				if newItem.length = invalid and length > 0
					newItem.length = length
				end if

				'PrintAA(newItem)
				result.Push(newItem)
			else if item.enclosure.Count() > 0
				' we didn't find any media:content tags, try the enclosure tag
				newStream = {
					url:	ValidStr(item.enclosure@url)
				}

				newItem.streams.Push(newStream)

				'PrintAA(newItem)
				result.Push(newItem)
			end if
		next
	end if

	return result
end function


Update stream url

Update stream url

Use the new stream url

sub Main()
	app = CreateObject( "roAppManager" )
	app.setTheme( LoadTheme() )
	categories = LoadConfig()

	CreatePosterScreen(categories).showScreen()
end sub

function LoadTheme()
	theme = CreateObject( "roAssociativeArray" )
	theme.OverhangSliceSD = "pkg:/images/Overhang_Background_SD.png"
	theme.OverhangSliceHD = "pkg:/images/Overhang_Background_HD.png"
	theme.OverhanglogoHD = "pkg:/images/Logo_Overhang_JB_HD.png"
	theme.OverhanglogoSD = "pkg:/images/Logo_Overhang_JB_SD.png"

	theme.OverhangPrimaryLogoOffsetHD_X = "60"
	theme.OverhangPrimaryLogoOffsetHD_Y = "20"

	theme.OverhangPrimaryLogoOffsetSD_X = "40"
	theme.OverhangPrimaryLogoOffsetSD_Y = "20"

	backgroundColor = "#E0E0E0"
	primaryTextColor = "#333333"
	secondaryTextColor = "#666666"
	tertiaryTextColor = "#999999"

	theme.backgroundColor = ValidStr( backgroundColor )
	theme.breadcrumbTextLeft = ValidStr( backgroundColor )
	theme.breadcrumbDelimiter = ValidStr( backgroundColor )
	theme.breadcrumbTextRight = ValidStr( backgroundColor )

	theme.posterScreenLine1Text = ValidStr( primaryTextColor )
	theme.posterScreenLine2Text = ValidStr( secondaryTextColor )
	theme.episodeSynopsisText = ValidStr( secondaryTextColor )

	theme.springboardTitleText = ValidStr( primaryTextColor )
	theme.springboardSynopsisColor = ValidStr( secondaryTextColor )
	theme.springboardRuntimeColor = ValidStr( tertiaryTextColor )
	theme.springboardDirectorColor = ValidStr( tertiaryTextColor )
	theme.springboardDirectorLabelColor = ValidStr( tertiaryTextColor )
	theme.springboardActorColor = ValidStr( tertiaryTextColor )

	return theme
end function

function LoadConfig()
	result = []

	result.push( GetLiveStream() )

	opml = CreateObject( "roXMLElement" )
	raw = NWM_UT_GetStringFromURL( "http://roku.jupitercolony.com/opml.xml" ) ' Pull configuration from a remote config file
	if opml.Parse( raw )
		for each category in opml.body.outline
			result.Push( BuildCategory( category ) )
		next
	else ' Fallback if cannot contact server, find file, or parse
		raw = ReadASCIIFile("pkg:/config.opml") ' Pull configuration from a local config file
		if opml.Parse( raw )
			for each category in opml.body.outline
				result.Push( BuildCategory( category ) )
			next
		end if
		ShowMessageDialog( "Cannot Connect", "Cannot connect to server. Please check your connection and try again." )
	end if

	result.push( GetQualityIcon() )

	return result
end function

function BuildCategory( category )
	result = {
		title:			ValidStr( category@title )
		shortDescriptionLine1:	ValidStr( category@title )
		shortDescriptionLine2:	ValidStr( category@subtitle )
		sdPosterURL:		ValidStr( category@img )
		hdPosterURL:		ValidStr( category@img )
		url:			ValidStr( category@url )
		categories:		[]
	}

	if category@hqfeed <> invalid
		result.url = ValidStr( category@hqfeed )
	end if

	quality = RegRead( "quality" )
	if quality <> invalid AND strtoi( ValidStr( quality ) ) = 1
		if category@lqfeed <> invalid
			result.url = ValidStr( category@lqfeed )
		end if
	end if

	if category.outline.Count() > 0
		for each subCategory in category.outline
			result.categories.Push( BuildCategory( subCategory ) )
		next
	end if

	return result
end function

function GetLiveStream()
	result = {
		screenTarget:		"livestream"
		calendar:		"http://roku.jupitercolony.com/live-calendar.xml"
		live:			true
		title:			"Live Stream"
		shortDescriptionLine1:	"Live Stream"
		shortDescriptionLine2:	"Watch Jupiter Broadcasting Live!"
		sdPosterURL:		"pkg:/images/mm_icon_focus_sd.png"
		hdPosterURL:		"pkg:/images/mm_icon_focus_hd.png"
		contentType:		"episode"
		streamurls:		["http://jblive.videocdn.scaleengine.net/jb-live/play/jblive.stream/playlist.m3u8"]
		streamformat:		"hls"
		streamqualities:	["SD"]
		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:		[]
	}

	return result
end function

function GetQualityIcon()
	result = {
		screenTarget:		"quality"
		title:			"Quality"
		shortDescriptionLine1:	"Quality"
		shortDescriptionLine2:	"Currently set to HQ"
		sdPosterURL:		"pkg:/images/settings.png"
		hdPosterURL:		"pkg:/images/settings.png"
		contentType:		"episode"
		categories:		[]
	}

	quality = RegRead( "quality" )
	if( quality <> invalid AND strtoi( ValidStr( quality ) ) = 1 )
		result.shortDescriptionLine2 = "Currently set to LQ (mobile quality)"
	end if

	return result
end function

sub Main()
	app = CreateObject( "roAppManager" )
	app.setTheme( LoadTheme() )
	categories = LoadConfig()

	CreatePosterScreen(categories).showScreen()
end sub

function LoadTheme()
	theme = CreateObject( "roAssociativeArray" )
	theme.OverhangSliceSD = "pkg:/images/Overhang_Background_SD.png"
	theme.OverhangSliceHD = "pkg:/images/Overhang_Background_HD.png"
	theme.OverhanglogoHD = "pkg:/images/Logo_Overhang_JB_HD.png"
	theme.OverhanglogoSD = "pkg:/images/Logo_Overhang_JB_SD.png"

	theme.OverhangPrimaryLogoOffsetHD_X = "60"
	theme.OverhangPrimaryLogoOffsetHD_Y = "20"

	theme.OverhangPrimaryLogoOffsetSD_X = "40"
	theme.OverhangPrimaryLogoOffsetSD_Y = "20"

	backgroundColor = "#E0E0E0"
	primaryTextColor = "#333333"
	secondaryTextColor = "#666666"
	tertiaryTextColor = "#999999"

	theme.backgroundColor = ValidStr( backgroundColor )
	theme.breadcrumbTextLeft = ValidStr( backgroundColor )
	theme.breadcrumbDelimiter = ValidStr( backgroundColor )
	theme.breadcrumbTextRight = ValidStr( backgroundColor )

	theme.posterScreenLine1Text = ValidStr( primaryTextColor )
	theme.posterScreenLine2Text = ValidStr( secondaryTextColor )
	theme.episodeSynopsisText = ValidStr( secondaryTextColor )

	theme.springboardTitleText = ValidStr( primaryTextColor )
	theme.springboardSynopsisColor = ValidStr( secondaryTextColor )
	theme.springboardRuntimeColor = ValidStr( tertiaryTextColor )
	theme.springboardDirectorColor = ValidStr( tertiaryTextColor )
	theme.springboardDirectorLabelColor = ValidStr( tertiaryTextColor )
	theme.springboardActorColor = ValidStr( tertiaryTextColor )

	return theme
end function

function LoadConfig()
	result = []

	result.push( GetLiveStream() )

	opml = CreateObject( "roXMLElement" )
	raw = NWM_UT_GetStringFromURL( "http://roku.jupitercolony.com/opml.xml" ) ' Pull configuration from a remote config file
	if opml.Parse( raw )
		for each category in opml.body.outline
			result.Push( BuildCategory( category ) )
		next
	else ' Fallback if cannot contact server, find file, or parse
		raw = ReadASCIIFile("pkg:/config.opml") ' Pull configuration from a local config file
		if opml.Parse( raw )
			for each category in opml.body.outline
				result.Push( BuildCategory( category ) )
			next
		end if
		ShowMessageDialog( "Cannot Connect", "Cannot connect to server. Please check your connection and try again." )
	end if

	result.push( GetQualityIcon() )

	return result
end function

function BuildCategory( category )
	result = {
		title:			ValidStr( category@title )
		shortDescriptionLine1:	ValidStr( category@title )
		shortDescriptionLine2:	ValidStr( category@subtitle )
		sdPosterURL:		ValidStr( category@img )
		hdPosterURL:		ValidStr( category@img )
		url:			ValidStr( category@url )
		categories:		[]
	}

	if category@hqfeed <> invalid
		result.url = ValidStr( category@hqfeed )
	end if

	quality = RegRead( "quality" )
	if quality <> invalid AND strtoi( ValidStr( quality ) ) = 1
		if category@lqfeed <> invalid
			result.url = ValidStr( category@lqfeed )
		end if
	end if

	if category.outline.Count() > 0
		for each subCategory in category.outline
			result.categories.Push( BuildCategory( subCategory ) )
		next
	end if

	return result
end function

function GetLiveStream()
	result = {
		screenTarget:		"livestream"
		calendar:		"http://roku.jupitercolony.com/live-calendar.xml"
		live:			true
		title:			"Live Stream"
		shortDescriptionLine1:	"Live Stream"
		shortDescriptionLine2:	"Watch Jupiter Broadcasting Live!"
		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"]
		streamformat:		"hls"
		streamqualities:	["SD"]
		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:		[]
	}

	return result
end function

function GetQualityIcon()
	result = {
		screenTarget:		"quality"
		title:			"Quality"
		shortDescriptionLine1:	"Quality"
		shortDescriptionLine2:	"Currently set to HQ"
		sdPosterURL:		"pkg:/images/settings.png"
		hdPosterURL:		"pkg:/images/settings.png"
		contentType:		"episode"
		categories:		[]
	}

	quality = RegRead( "quality" )
	if( quality <> invalid AND strtoi( ValidStr( quality ) ) = 1 )
		result.shortDescriptionLine2 = "Currently set to LQ (mobile quality)"
	end if

	return result
end function


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

sub test_broken (t as Object)
    t.assertTrue(False)
end sub

' #################################################################
' ###   brsHamcrest   ###   github.com/imbenjamin/brsHamcrest   ###
' #################################################################
'                 Copyright (c) 2016 Benjamin Hill

' SETUP / TEARDOWN

'Set up the unit tests
'
'@return {Object} The test helper
function setup_brsHamcrest_Helpers () as Object
    test = {
        knownArray: []
        knownString: "foo"
        ifEnum: "ifEnum"
        unknownInterface: "ifFoo"
    }
    return test
end function

'Clean up after running a unit test
function teardown_brsHamcrest_Helpers () as Void

end function

' UNIT TESTS

' HasInterface()
sub test_HasInterface_true (t as Object)
    test = setup_brsHamcrest_Helpers()

    'GIVEN'
    testObj = test.knownArray
    testInterface = test.ifEnum

    'WHEN'
    result = HasInterface(testObj, testInterface)

    'THEN'
    t.assertTrue(result)

    teardown_brsHamcrest_Helpers()
end sub

sub test_HasInterface_false (t as Object)
    test = setup_brsHamcrest_Helpers()

    'GIVEN'
    testObj = test.knownString
    testInterface = test.ifEnum

    'WHEN'
    result = HasInterface(testObj, testInterface)

    'THEN'
    t.assertFalse(result)

    teardown_brsHamcrest_Helpers()
end sub

sub test_HasInterface_false_unknownInterface (t as Object)
    test = setup_brsHamcrest_Helpers()

    'GIVEN'
    testObj = test.knownString
    testInterface = test.unknownInterface

    'WHEN'
    result = HasInterface(testObj, testInterface)

    'THEN'
    t.assertFalse(result)

    teardown_brsHamcrest_Helpers()
end sub


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

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

' #################################################################
' ###   brsHamcrest   ###   github.com/imbenjamin/brsHamcrest   ###
' #################################################################

'Decorates another Matcher, retaining its behaviour, but allowing tests to be slightly more expressive.
'If passed an array, then is() acts as a shortcut to allOf()
'
'Example:
'assertThat(foo, is(aString()))
'assertThat(foo, is([aString(), endsWithString("bar")]))
'
'@param matcher {Object<Matcher>} A Matcher
'@return {Object<Matcher>} A Matcher
function is (matcher as Object) as Object
    if type(matcher) = "roArray" then return allOf(matcher)
    return matcher
end function

'A matcher that has the correct structure, but returns true to anything, for the purposes of integration with frameworks
'
'Example:
'assertThat(foo, anything())
'
'@return {Object<Matcher>} A Matcher
function anything () as Object
    matcher = BaseMatcher()

    matcher.append({
        doMatch: function (target as Dynamic) as Boolean
            return true
        end function
        })

    return matcher
end function

'Creates a matcher that wraps an existing matcher, but inverts the logic by which it will match.
'If passed an array, isNot() acts as a shortcut to noneOf()
'
'Example:
'assertThat(foo, isNot(aString()))
'assertThat(foo, isNot([aString(), aNumber()]))
'
'@param matcher {Object<Matcher>} A Matcher
'@return {Object<Matcher>} A Matcher
function isNot (matcher as Object) as Object
    if type(matcher) = "roArray" then return noneOf(matcher)

    retVal = Invalid
    if (matcher.CLASS_TYPE = "Matcher")
        newMatcher = matcher

        newMatcher.doMatchOrig = newMatcher.doMatch
        newMatcher.doMatch = function (target as Dynamic) as Boolean
            return (NOT m.doMatchOrig(target))
        end function

        retVal = newMatcher
    else
        HamcrestError("Type Mismatch: Expected a Matcher and encountered a "+type(matcher))
    end if
    return retVal
end function

'Creates a matcher that checks against array of matchers, only passing if all matchers are positive.
'
'Example:
'assertThat(foo, is(allOf([aString(), inCollection(bar)])))
'
'@param arrayOfMatchers {Array<Matcher>} An array of Matchers
'@return {Object<Matcher>} A Matcher
function allOf (arrayOfMatchers as Object) as Object
    matcher = BaseMatcher()

    matcher.append({
        arrayOfMatchers: arrayOfMatchers

        doMatch: function (target as Dynamic) as Boolean
            failure = false
            if (type(m.arrayOfMatchers) = "roArray")
                for each matcherObj in m.arrayOfMatchers
                    if (matcherObj.CLASS_TYPE = "Matcher")
                        failure = (NOT matcherObj.doMatch(target))
                    else
                        HamcrestError("Type Mismatch: Expected a Matcher and encountered a "+type(matcherObj))
                    end if
                end for
            else
                HamcrestError("Type Mismatch: Expected an Array and encountered a "+type(m.arrayOfMatchers))
            end if
            return (NOT failure)
        end function
    })

    return matcher
end function

'Creates a matcher that checks against array of matchers, passing if any of the matchers are positive.
'
'Example:
'assertThat(foo, is(anyOf([aString(), inCollection(bar)])))
'
'@param arrayOfMatchers {Array<Matcher>} An array of Matchers
'@return {Object<Matcher>} A Matcher
function anyOf (arrayOfMatchers as Object) as Object
    matcher = BaseMatcher()

    matcher.append({
        arrayOfMatchers: arrayOfMatchers

        doMatch: function (target as Dynamic) as Boolean
            if (type(m.arrayOfMatchers) = "roArray")
                for each matcherObj in m.arrayOfMatchers
                    if (matcherObj.doMatch(target))
                        return true
                    end if
                end for
            else
                HamcrestError("Type Mismatch: Expected an Array and encountered a "+type(m.arrayOfMatchers))
            end if

            return false
        end function
    })

    return matcher
end function

'Creates a matcher that checks against array of matchers, only passing if none of the matchers are positive.
'
'Example:
'assertThat(foo, is(noneOf([aString(), inCollection(bar)])))
'
'@param arrayOfMatchers {Array<Matcher>} An array of Matchers
'@return {Object<Matcher>} A Matcher
function noneOf(arrayOfMatchers as Object) as Object
    matcher = BaseMatcher()

    matcher.append({
        arrayOfMatchers: arrayOfMatchers

        doMatch: function (target as Dynamic) as Boolean
            if (type(m.arrayOfMatchers) = "roArray")
                for each matcherObj in m.arrayOfMatchers
                    if (matcherObj.doMatch(target))
                        return false
                    end if
                end for
            else
                HamcrestError("Type Mismatch: Expected an Array and encountered a "+type(m.arrayOfMatchers))
            end if

            return true
        end function
    })

    return matcher
end function

'Creates a matcher that does an exact match, only passing if an exact match of both value and type are made.
'
'Example:
'assertThat(foo, is(equalTo(bar)))
'
'@param comparison - any value
'@return {Object<Matcher>} A Matcher
function equalTo(comparison as Dynamic) as Object
    matcher = BaseMatcher()

    matcher.append({
        comparison: comparison

        doMatch: function (target as Dynamic) as Boolean
            targetType = m._normaliseType(type(target))
            comparisonType = m._normaliseType(type(m.comparison))

            if (targetType = comparisonType)
                if (targetType = "roArray")
                    if (target.count() = m.comparison.count())
                        for i=0 to target.count()-1 Step 1
                            if (equalTo(m.comparison[i]).doMatch(target[i]) = false) return false
                        end for
                    else
                        return false
                    end if
                else if (targetType = "roAssociativeArray")
                    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
                    else
                        return false
                    end if

                else if (targetType = "roBoolean" OR targetType = "roDouble" OR targetType = "roFloat" OR targetType = "roInteger" OR targetType = "roLongInteger" OR targetType = "roString" OR targetType = "roInvalid")
                    return (target = m.comparison)

                else if (targetType = "roFunction")
                    return (target.toStr() = m.comparison.toStr())

                else
                    return (targetType = comparisonType)
                end if
            else
                return false
            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

                types = {
                    "roArray": "roArray"
                    "roAssociativeArray": "roAssociativeArray"
                    "roBoolean": "roBoolean"
                    "Boolean": "roBoolean"
                    "roDouble": "roDouble"
                    "Double": "roDouble"
                    "roIntrinsicDouble": "roDouble"
                    "roFloat": "roFloat"
                    "Float": "roFloat"
                    "roFunction": "roFunction"
                    "Function": "roFunction"
                    "roInteger": "roInteger"
                    "roInt": "roInteger"
                    "Integer": "roInteger"
                    "roLongInteger": "roLongInteger"
                    "LongInteger": "roLongInteger"
                    "roString": "roString"
                    "String": "roString"
                    "roInvalid": "roInvalid"
                    "Invalid": "roInvalid"
                }

                if (types.DoesExist(typeIn))
                    typeOut = types[typeIn]
                else
                    HamcrestError("Unsupported type cannot be normalised")
                    typeOut = "<ERROR:UNSUPPORTED_TYPE>"
                end if

                return typeOut
        end function
    })

    return matcher
end function

' #################################################################
' ###   brsHamcrest   ###   github.com/imbenjamin/brsHamcrest   ###
' #################################################################

'Decorates another Matcher, retaining its behaviour, but allowing tests to be slightly more expressive.
'If passed an array, then is() acts as a shortcut to allOf()
'
'Example:
'assertThat(foo, is(aString()))
'assertThat(foo, is([aString(), endsWithString("bar")]))
'
'@param matcher {Object<Matcher>} A Matcher
'@return {Object<Matcher>} A Matcher
function is (matcher as Object) as Object
    if type(matcher) = "roArray" then return allOf(matcher)
    return matcher
end function

'A matcher that has the correct structure, but returns true to anything, for the purposes of integration with frameworks
'
'Example:
'assertThat(foo, anything())
'
'@return {Object<Matcher>} A Matcher
function anything () as Object
    matcher = BaseMatcher()

    matcher.append({
        doMatch: function (target as Dynamic) as Boolean
            return true
        end function
        })

    return matcher
end function

'Creates a matcher that wraps an existing matcher, but inverts the logic by which it will match.
'If passed an array, isNot() acts as a shortcut to noneOf()
'
'Example:
'assertThat(foo, isNot(aString()))
'assertThat(foo, isNot([aString(), aNumber()]))
'
'@param matcher {Object<Matcher>} A Matcher
'@return {Object<Matcher>} A Matcher
function isNot (matcher as Object) as Object
    if type(matcher) = "roArray" then return noneOf(matcher)

    retVal = Invalid
    if (matcher.CLASS_TYPE = "Matcher")
        newMatcher = matcher

        newMatcher.doMatchOrig = newMatcher.doMatch
        newMatcher.doMatch = function (target as Dynamic) as Boolean
            return (NOT m.doMatchOrig(target))
        end function

        retVal = newMatcher
    else
        HamcrestError("Type Mismatch: Expected a Matcher and encountered a "+type(matcher))
    end if
    return retVal
end function

'Creates a matcher that checks against array of matchers, only passing if all matchers are positive.
'
'Example:
'assertThat(foo, is(allOf([aString(), inCollection(bar)])))
'
'@param arrayOfMatchers {Array<Matcher>} An array of Matchers
'@return {Object<Matcher>} A Matcher
function allOf (arrayOfMatchers as Object) as Object
    matcher = BaseMatcher()

    matcher.append({
        arrayOfMatchers: arrayOfMatchers

        doMatch: function (target as Dynamic) as Boolean
            failure = false
            if (type(m.arrayOfMatchers) = "roArray")
                for each matcherObj in m.arrayOfMatchers
                    if (matcherObj.CLASS_TYPE = "Matcher")
                        failure = (NOT matcherObj.doMatch(target))
                    else
                        HamcrestError("Type Mismatch: Expected a Matcher and encountered a "+type(matcherObj))
                    end if
                end for
            else
                HamcrestError("Type Mismatch: Expected an Array and encountered a "+type(m.arrayOfMatchers))
            end if
            return (NOT failure)
        end function
    })

    return matcher
end function

'Creates a matcher that checks against array of matchers, passing if any of the matchers are positive.
'
'Example:
'assertThat(foo, is(anyOf([aString(), inCollection(bar)])))
'
'@param arrayOfMatchers {Array<Matcher>} An array of Matchers
'@return {Object<Matcher>} A Matcher
function anyOf (arrayOfMatchers as Object) as Object
    matcher = BaseMatcher()

    matcher.append({
        arrayOfMatchers: arrayOfMatchers

        doMatch: function (target as Dynamic) as Boolean
            if (type(m.arrayOfMatchers) = "roArray")
                for each matcherObj in m.arrayOfMatchers
                    if (matcherObj.doMatch(target))
                        return true
                    end if
                end for
            else
                HamcrestError("Type Mismatch: Expected an Array and encountered a "+type(m.arrayOfMatchers))
            end if

            return false
        end function
    })

    return matcher
end function

'Creates a matcher that checks against array of matchers, only passing if none of the matchers are positive.
'
'Example:
'assertThat(foo, is(noneOf([aString(), inCollection(bar)])))
'
'@param arrayOfMatchers {Array<Matcher>} An array of Matchers
'@return {Object<Matcher>} A Matcher
function noneOf(arrayOfMatchers as Object) as Object
    matcher = BaseMatcher()

    matcher.append({
        arrayOfMatchers: arrayOfMatchers

        doMatch: function (target as Dynamic) as Boolean
            if (type(m.arrayOfMatchers) = "roArray")
                for each matcherObj in m.arrayOfMatchers
                    if (matcherObj.doMatch(target))
                        return false
                    end if
                end for
            else
                HamcrestError("Type Mismatch: Expected an Array and encountered a "+type(m.arrayOfMatchers))
            end if

            return true
        end function
    })

    return matcher
end function

'Creates a matcher that does an exact match, only passing if an exact match of both value and type are made.
'
'Example:
'assertThat(foo, is(equalTo(bar)))
'
'@param comparison - any value
'@return {Object<Matcher>} A Matcher
function equalTo(comparison as Dynamic) as Object
    matcher = BaseMatcher()

    matcher.append({
        comparison: comparison

        doMatch: function (target as Dynamic) as Boolean
            targetType = m._normaliseType(type(target))
            comparisonType = m._normaliseType(type(m.comparison))

            if (targetType = comparisonType)
                if (targetType = "roArray")
                    if (target.count() = m.comparison.count())
                        for i=0 to target.count()-1 Step 1
                            if (equalTo(m.comparison[i]).doMatch(target[i]) = false) return false
                        end for
                    else
                        return false
                    end if
                else if (targetType = "roAssociativeArray")
                    if (target.Count() = m.comparison.Count())
                        for each key in target
                            if (equalTo(m.comparison.LookupCI(key)).doMatch(target.LookupCI(key)) = false) return false
                        end for
                    else
                        return false
                    end if

                else if (targetType = "roBoolean" OR targetType = "roDouble" OR targetType = "roFloat" OR targetType = "roInteger" OR targetType = "roLongInteger" OR targetType = "roString" OR targetType = "roInvalid")
                    return (target = m.comparison)

                else if (targetType = "roFunction")
                    return (target.toStr() = m.comparison.toStr())

                else
                    return (targetType = comparisonType)
                end if
            else
                return false
            end if

            return true
        end function

        _normaliseType: function (typeIn as String) as String

                types = {
                    "roArray": "roArray"
                    "roAssociativeArray": "roAssociativeArray"
                    "roBoolean": "roBoolean"
                    "Boolean": "roBoolean"
                    "roDouble": "roDouble"
                    "Double": "roDouble"
                    "roIntrinsicDouble": "roDouble"
                    "roFloat": "roFloat"
                    "Float": "roFloat"
                    "roFunction": "roFunction"
                    "Function": "roFunction"
                    "roInteger": "roInteger"
                    "roInt": "roInteger"
                    "Integer": "roInteger"
                    "roLongInteger": "roLongInteger"
                    "LongInteger": "roLongInteger"
                    "roString": "roString"
                    "String": "roString"
                    "roInvalid": "roInvalid"
                    "Invalid": "roInvalid"
                }

                if (types.DoesExist(typeIn))
                    typeOut = types[typeIn]
                else
                    HamcrestError("Unsupported type cannot be normalised")
                    typeOut = "<ERROR:UNSUPPORTED_TYPE>"
                end if

                return typeOut
        end function
    })

    return matcher
end function


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

'**********************************************************
'**  Video Player Example Application - Detail Screen
'**  November 2009
'**  Copyright (c) 2009 Roku Inc. All Rights Reserved.
'**********************************************************

Function preShowDetailScreen(breadA=invalid, breadB=invalid) As Object
    port=CreateObject("roMessagePort")
    screen = CreateObject("roSpringboardScreen")
    screen.SetDescriptionStyle("movie")
    screen.SetMessagePort(port)
    if breadA<>invalid and breadB<>invalid then
        screen.SetBreadcrumbText(breadA, breadB)
    end if

    screen.setAdDisplayMode("flat-square")
    screen.SetStaticRatingEnabled(FALSE)

    return screen

End Function

'***************************************************************
'** The show detail screen (springboard) is where the user sees
'** the details for a show and is allowed to select a show to
'** begin playback.  This is the main event loop for that screen
'** and where we spend our time waiting until the user presses a
'** button and then we decide how best to handle the event.
'***************************************************************
Function showDetailScreen(screen As Object, showList As Object, showIndex as Integer) As Integer

    print "showDetailScreen"

    if validateParam(screen, "roSpringboardScreen", "showDetailScreen") = false return -1
    if validateParam(showList, "roArray", "showDetailScreen") = false return -1

    refreshShowDetail(screen, showList, showIndex)

    'remote key id's for left/right navigation
    remoteKeyLeft  = 4
    remoteKeyRight = 5

    while true
        msg = wait(0, screen.GetMessagePort())

        if type(msg) = "roSpringboardScreenEvent" then
            if msg.isScreenClosed()
                print "Screen closed"
                exit while
            else if msg.isRemoteKeyPressed()
                print "Remote key pressed"
                if msg.GetIndex() = remoteKeyLeft then
                        showIndex = getPrevShow(showList, showIndex)
                        if showIndex <> -1
                            refreshShowDetail(screen, showList, showIndex)
                        end if
                else if msg.GetIndex() = remoteKeyRight
                    showIndex = getNextShow(showList, showIndex)
                        if showIndex <> -1
                           refreshShowDetail(screen, showList, showIndex)
                        end if
                endif
            else if msg.isButtonPressed()
                print "ButtonPressed"
                print "ButtonPressed"
                if msg.GetIndex() = 1
                    PlayStart = RegRead(showList[showIndex].ContentId)
                    if PlayStart <> invalid then
                        showList[showIndex].PlayStart = PlayStart.ToInt()
                    endif
                    if showList[showIndex].LiveStream = "true" then
                      showList[showIndex].PlayStart = 1800
                    else
                      showList[showIndex].PlayStart = 0
                    endif
                    showVideoScreen(showList[showIndex])
                    refreshShowDetail(screen,showList,showIndex)
                endif
                if msg.GetIndex() = 2
                    if showList[showIndex].LiveStream = "true" then
                      showList[showIndex].PlayStart = 1800
                    else
                      showList[showIndex].PlayStart = 0
                    endif
                    showVideoScreen(showList[showIndex])
                    refreshShowDetail(screen,showList,showIndex)
                endif
                if msg.GetIndex() = 3
                endif
                print "Button pressed: "; msg.GetIndex(); " " msg.GetData()
            end if
        else
            print "Unexpected message class: "; type(msg)
        end if
    end while

    return showIndex

End Function

'**************************************************************
'** Refresh the contents of the show detail screen. This may be
'** required on initial entry to the screen or as the user moves
'** left/right on the springboard.  When the user is on the
'** springboard, we generally let them press left/right arrow keys
'** to navigate to the previous/next show in a circular manner.
'** When leaving the screen, the should be positioned on the
'** corresponding item in the poster screen matching the current show
'**************************************************************
Function refreshShowDetail(screen As Object, showList As Object, showIndex as Integer) As Integer

    if validateParam(screen, "roSpringboardScreen", "refreshShowDetail") = false return -1
    if validateParam(showList, "roArray", "refreshShowDetail") = false return -1

    show = showList[showIndex]

    'Uncomment this statement to dump the details for each show
    'PrintAA(show)

    screen.ClearButtons()

    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)

    globals = getGlobalAA()

    ' set some analytics
    globals.analytics = Analytics()

    showTitle = show.Series + " - " + show.Title

    globals.analytics.trackEvent("pageview", show.ItemUrl, showTitle, "", "", "")

    screen.Show()

End Function

'********************************************************
'** Get the next item in the list and handle the wrap
'** around case to implement a circular list for left/right
'** navigation on the springboard screen
'********************************************************
Function getNextShow(showList As Object, showIndex As Integer) As Integer
    if validateParam(showList, "roArray", "getNextShow") = false return -1

    nextIndex = showIndex + 1
    if nextIndex >= showList.Count() or nextIndex < 0 then
       nextIndex = 0
    end if

    show = showList[nextIndex]
    if validateParam(show, "roAssociativeArray", "getNextShow") = false return -1

    return nextIndex
End Function

'********************************************************
'** Get the previous item in the list and handle the wrap
'** around case to implement a circular list for left/right
'** navigation on the springboard screen
'********************************************************
Function getPrevShow(showList As Object, showIndex As Integer) As Integer
    if validateParam(showList, "roArray", "getPrevShow") = false return -1

    prevIndex = showIndex - 1
    if prevIndex < 0 or prevIndex >= showList.Count() then
        if showList.Count() > 0 then
            prevIndex = showList.Count() - 1
        else
            return -1
        end if
    end if

    show = showList[prevIndex]
    if validateParam(show, "roAssociativeArray", "getPrevShow") = false return -1

    return prevIndex
End Function

'**********************************************************
'**  Video Player Example Application - Detail Screen
'**  November 2009
'**  Copyright (c) 2009 Roku Inc. All Rights Reserved.
'**********************************************************

Function preShowDetailScreen(breadA=invalid, breadB=invalid) As Object
    port=CreateObject("roMessagePort")
    screen = CreateObject("roSpringboardScreen")
    screen.SetDescriptionStyle("movie")
    screen.SetMessagePort(port)
    if breadA<>invalid and breadB<>invalid then
        screen.SetBreadcrumbText(breadA, breadB)
    end if

    screen.setAdDisplayMode("flat-square")
    screen.SetStaticRatingEnabled(FALSE)

    return screen

End Function

'***************************************************************
'** The show detail screen (springboard) is where the user sees
'** the details for a show and is allowed to select a show to
'** begin playback.  This is the main event loop for that screen
'** and where we spend our time waiting until the user presses a
'** button and then we decide how best to handle the event.
'***************************************************************
Function showDetailScreen(screen As Object, showList As Object, showIndex as Integer) As Integer

    print "showDetailScreen"

    if validateParam(screen, "roSpringboardScreen", "showDetailScreen") = false return -1
    if validateParam(showList, "roArray", "showDetailScreen") = false return -1

    refreshShowDetail(screen, showList, showIndex)

    'remote key id's for left/right navigation
    remoteKeyLeft  = 4
    remoteKeyRight = 5

    while true
        msg = wait(0, screen.GetMessagePort())

        if type(msg) = "roSpringboardScreenEvent" then
            if msg.isScreenClosed()
                print "Screen closed"
                exit while
            else if msg.isRemoteKeyPressed()
                print "Remote key pressed"
                if msg.GetIndex() = remoteKeyLeft then
                        showIndex = getPrevShow(showList, showIndex)
                        if showIndex <> -1
                            refreshShowDetail(screen, showList, showIndex)
                        end if
                else if msg.GetIndex() = remoteKeyRight
                    showIndex = getNextShow(showList, showIndex)
                        if showIndex <> -1
                           refreshShowDetail(screen, showList, showIndex)
                        end if
                endif
            else if msg.isButtonPressed()
                print "ButtonPressed"
                print "ButtonPressed"
                if msg.GetIndex() = 1
                    PlayStart = RegRead(showList[showIndex].ContentId)
                    if PlayStart <> invalid then
                        showList[showIndex].PlayStart = PlayStart.ToInt()
                    endif
                    if showList[showIndex].LiveStream = "true" then
                      showList[showIndex].PlayStart = 1800
                    else
                      showList[showIndex].PlayStart = 0
                    endif
                    showVideoScreen(showList[showIndex])
                    refreshShowDetail(screen,showList,showIndex)
                endif
                if msg.GetIndex() = 2
                    if showList[showIndex].LiveStream = "true" then
                      showList[showIndex].PlayStart = 1800
                    else
                      showList[showIndex].PlayStart = 0
                    endif
                    showVideoScreen(showList[showIndex])
                    refreshShowDetail(screen,showList,showIndex)
                endif
                if msg.GetIndex() = 3
                endif
                print "Button pressed: "; msg.GetIndex(); " " msg.GetData()
            end if
        else
            print "Unexpected message class: "; type(msg)
        end if
    end while

    return showIndex

End Function

'**************************************************************
'** Refresh the contents of the show detail screen. This may be
'** required on initial entry to the screen or as the user moves
'** left/right on the springboard.  When the user is on the
'** springboard, we generally let them press left/right arrow keys
'** to navigate to the previous/next show in a circular manner.
'** When leaving the screen, the should be positioned on the
'** corresponding item in the poster screen matching the current show
'**************************************************************
Function refreshShowDetail(screen As Object, showList As Object, showIndex as Integer) As Integer

    if validateParam(screen, "roSpringboardScreen", "refreshShowDetail") = false return -1
    if validateParam(showList, "roArray", "refreshShowDetail") = false return -1

    show = showList[showIndex]

    'Uncomment this statement to dump the details for each show
    'PrintAA(show)

    screen.ClearButtons()
    screen.AddButton(1, "Play")

    screen.SetContent(show)

    globals = getGlobalAA()

    ' set some analytics
    globals.analytics = Analytics()

    showTitle = show.Series + " - " + show.Title

    globals.analytics.trackEvent("pageview", show.ItemUrl, showTitle, "", "", "")

    screen.Show()

End Function

'********************************************************
'** Get the next item in the list and handle the wrap
'** around case to implement a circular list for left/right
'** navigation on the springboard screen
'********************************************************
Function getNextShow(showList As Object, showIndex As Integer) As Integer
    if validateParam(showList, "roArray", "getNextShow") = false return -1

    nextIndex = showIndex + 1
    if nextIndex >= showList.Count() or nextIndex < 0 then
       nextIndex = 0
    end if

    show = showList[nextIndex]
    if validateParam(show, "roAssociativeArray", "getNextShow") = false return -1

    return nextIndex
End Function

'********************************************************
'** Get the previous item in the list and handle the wrap
'** around case to implement a circular list for left/right
'** navigation on the springboard screen
'********************************************************
Function getPrevShow(showList As Object, showIndex As Integer) As Integer
    if validateParam(showList, "roArray", "getPrevShow") = false return -1

    prevIndex = showIndex - 1
    if prevIndex < 0 or prevIndex >= showList.Count() then
        if showList.Count() > 0 then
            prevIndex = showList.Count() - 1
        else
            return -1
        end if
    end if

    show = showList[prevIndex]
    if validateParam(show, "roAssociativeArray", "getPrevShow") = false return -1

    return prevIndex
End Function


Convert SpringboardScreen to use WatchedStatusTracker.

function CreateSpringboardScreen(episodes, selectedEpisodeIndex)
	instance = CreateObject("roAssociativeArray")

	instance.screen = CreateObject("roSpringboardScreen")
	instance.messagePort = CreateObject("roMessagePort")
	instance.screen.SetMessagePort(instance.messagePort)
	instance.watchedStatusTracker = WatchedStatusTracker()
	instance.lastMessage = invalid
	instance.screen.SetStaticRatingEnabled(false)

	instance.episodes = episodes
	instance.selectedEpisodeIndex = selectedEpisodeIndex

	instance.getSelectedEpisode = function()
		return m.episodes[m.selectedEpisodeIndex]
	end function

	instance.showScreen = function()
		m.showAds()
		m.showEpisode()
		m.waitForInput()

		return m.selectedEpisodeIndex
	end function

	instance.showAds = function()
		'Don't show ads, really
		'm.screen.setAdURL(sdAd, hdAd)
		'm.screen.setAdSelectable(false)
		x = 0 'Don't break the editor
	end function

	instance.showEpisode = function()
		episode = m.getSelectedEpisode()
	    m.setButtons()

	    m.swapPoster()
	    m.screen.SetContent(episode)
	    m.screen.Show()
	    m.swapPoster()
	end function

	instance.swapPoster = function()
		episode = m.getSelectedEpisode()
		tmp = episode.sdPosterURL
		if episode.sdPosterURL_hq <> invalid and episode.sdPosterURL_hq <> ""
			episode.sdPosterURL = episode.sdPosterURL_hq
			episode.sdPosterURL_hq = tmp
		end if
		tmp = episode.hdPosterURL
		if episode.hdPosterURL_hq <> invalid and episode.hdPosterURL_hq <> ""
			episode.hdPosterURL = episode.hdPosterURL_hq
			episode.hdPosterURL_hq = tmp
		end if
	end function

	instance.setButtons = function()
		episode = m.getSelectedEpisode()
		m.screen.ClearButtons()
		if m.isPartiallyWatched() then
			m.screen.AddButton(1, "Resume Playing")
			m.screen.AddButton(2, "Play from Beginning")
		else
			m.screen.AddButton(2, "Play")
		end if
	end function

	instance.isPartiallyWatched = function()
		episode = m.getSelectedEpisode()
		return m.watchedStatusTracker.hasProgress(episode.title)
	end function

	instance.waitForInput = function()
		episode = m.getSelectedEpisode()
		while m.hasNonExitMessage()
			m.processMessage()
		end while
	end function

	instance.processMessage = function()
		msg = m.getLastMessage()
		if msg = invalid then
			return invalid
		end if

		if msg.isButtonPressed()
			m.processButtonPress()
		else if msg.isRemoteKeyPressed()
			m.processRemoteKeyPress()
		end if
	end function

	instance.getLastMessage = function()
		return m.lastMessage
	end function

	instance.processButtonPress = function()
		msg = m.getLastMessage()
		episode = m.getSelectedEpisode()
		if msg.GetIndex() = 1
			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)
		m.setButtons()
		m.screen.Show()
	end function

	instance.processRemoteKeyPress = function()
		msg = m.getLastMessage()
		if msg.GetIndex() = 4 ' LEFT
			m.movePreviousEpisode()
		else if msg.GetIndex() = 5 ' RIGHT
			m.moveNextEpisode()
		end if
		m.showEpisode()
	end function

	instance.hasNonExitMessage = function()
		m.lastMessage = wait(0, m.messagePort)
		return m.lastMessage = invalid or not m.lastMessage.isScreenClosed()
	end function

	instance.moveNextEpisode = function()
		if m.selectedEpisodeIndex = m.episodes.Count() - 1
			m.selectedEpisodeIndex = 0
		else
			m.selectedEpisodeIndex = m.selectedEpisodeIndex + 1
		end if

		return m.selectedEpisodeIndex
	end function

	instance.movePreviousEpisode = function()
		if m.selectedEpisodeIndex = 0
			m.selectedEpisodeIndex = m.episodes.Count() - 1
		else
			m.selectedEpisodeIndex = m.selectedEpisodeIndex - 1
		end if

		return m.selectedEpisodeIndex
	end function

	return instance
end function

function CreateSpringboardScreen(episodes, selectedEpisodeIndex)
	instance = CreateObject("roAssociativeArray")

	instance.screen = CreateObject("roSpringboardScreen")
	instance.messagePort = CreateObject("roMessagePort")
	instance.screen.SetMessagePort(instance.messagePort)
	instance.lastMessage = invalid
	instance.screen.SetStaticRatingEnabled(false)

	instance.episodes = episodes
	instance.selectedEpisodeIndex = selectedEpisodeIndex

	instance.getSelectedEpisode = function()
		return m.episodes[m.selectedEpisodeIndex]
	end function

	instance.showScreen = function()
		m.showAds()
		m.showEpisode()
		m.waitForInput()

		return m.selectedEpisodeIndex
	end function

	instance.showAds = function()
		'Don't show ads, really
		'm.screen.setAdURL(sdAd, hdAd)
		'm.screen.setAdSelectable(false)
		x = 0 'Don't break the editor
	end function

	instance.showEpisode = function()
		episode = m.getSelectedEpisode()
	    m.setButtons()

	    m.swapPoster()
	    m.screen.SetContent(episode)
	    m.screen.Show()
	    m.swapPoster()
	end function

	instance.swapPoster = function()
		episode = m.getSelectedEpisode()
		tmp = episode.sdPosterURL
		if episode.sdPosterURL_hq <> invalid and episode.sdPosterURL_hq <> ""
			episode.sdPosterURL = episode.sdPosterURL_hq
			episode.sdPosterURL_hq = tmp
		end if
		tmp = episode.hdPosterURL
		if episode.hdPosterURL_hq <> invalid and episode.hdPosterURL_hq <> ""
			episode.hdPosterURL = episode.hdPosterURL_hq
			episode.hdPosterURL_hq = tmp
		end if
	end function

	instance.setButtons = function()
		episode = m.getSelectedEpisode()
		m.screen.ClearButtons()
		if m.isPartiallyWatched() then
			m.screen.AddButton(1, "Resume Playing")
			m.screen.AddButton(2, "Play from Beginning")
		else
			m.screen.AddButton(2, "Play")
		end if
	end function

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

	instance.waitForInput = function()
		episode = m.getSelectedEpisode()
		while m.hasNonExitMessage()
			m.processMessage()
		end while
	end function

	instance.processMessage = function()
		msg = m.getLastMessage()
		if msg = invalid then
			return invalid
		end if

		if msg.isButtonPressed()
			m.processButtonPress()
		else if msg.isRemoteKeyPressed()
			m.processRemoteKeyPress()
		end if
	end function

	instance.getLastMessage = function()
		return m.lastMessage
	end function

	instance.processButtonPress = function()
		msg = m.getLastMessage()
		episode = m.getSelectedEpisode()
		if msg.GetIndex() = 1
			PlayStart = RegRead(episode.title)
			if PlayStart <> invalid then
				episode.PlayStart = PlayStart.ToInt()
			end if
		else if msg.GetIndex() = 2
			episode.PlayStart = 0
		end if
		ShowVideoScreen(episode)
		m.setButtons()
		m.screen.Show()
	end function

	instance.processRemoteKeyPress = function()
		msg = m.getLastMessage()
		if msg.GetIndex() = 4 ' LEFT
			m.movePreviousEpisode()
		else if msg.GetIndex() = 5 ' RIGHT
			m.moveNextEpisode()
		end if
		m.showEpisode()
	end function

	instance.hasNonExitMessage = function()
		m.lastMessage = wait(0, m.messagePort)
		return m.lastMessage = invalid or not m.lastMessage.isScreenClosed()
	end function

	instance.moveNextEpisode = function()
		if m.selectedEpisodeIndex = m.episodes.Count() - 1
			m.selectedEpisodeIndex = 0
		else
			m.selectedEpisodeIndex = m.selectedEpisodeIndex + 1
		end if

		return m.selectedEpisodeIndex
	end function

	instance.movePreviousEpisode = function()
		if m.selectedEpisodeIndex = 0
			m.selectedEpisodeIndex = m.episodes.Count() - 1
		else
			m.selectedEpisodeIndex = m.selectedEpisodeIndex - 1
		end if

		return m.selectedEpisodeIndex
	end function

	return instance
end function


Added information for live audio stream.

sub Main()
	LoadTheme()
	categories = LoadConfig()

	if categories.Count() > 1
		ShowPosterScreen( categories )
	else
		ShowEpisodeScreen( categories[0] )
	end if
end sub

sub LoadTheme()
	app = CreateObject( "roAppManager" )
	theme = CreateObject( "roAssociativeArray" )
	theme.OverhangSliceSD = "pkg:/images/Overhang_Slice_SD.png"
	theme.OverhangSliceHD = "pkg:/images/Overhang_Slice_HD.png"
	theme.OverhanglogoHD = "pkg:/images/Logo_Overhang_JB_HD.png"
	theme.OverhanglogoSD = "pkg:/images/Logo_Overhang_JB_SD.png"

	theme.OverhangPrimaryLogoOffsetHD_X = "60"
	theme.OverhangPrimaryLogoOffsetHD_Y = "40"

	theme.OverhangPrimaryLogoOffsetSD_X = "60"
	theme.OverhangPrimaryLogoOffsetSD_Y = "40"

	backgroundColor = "#E0E0E0"
	primaryTextColor = "#333333"
	secondaryTextColor = "#666666"
	tertiaryTextColor = "#999999"

	theme.backgroundColor = ValidStr( backgroundColor )
	theme.breadcrumbTextLeft = ValidStr( primaryTextColor )
	theme.breadcrumbDelimiter = ValidStr( primaryTextColor )
	theme.breadcrumbTextRight = ValidStr( primaryTextColor )

	theme.posterScreenLine1Text = ValidStr( primaryTextColor )
	theme.posterScreenLine2Text = ValidStr( secondaryTextColor )
	theme.episodeSynopsisText = ValidStr( secondaryTextColor )

	theme.springboardTitleText = ValidStr( primaryTextColor )
	theme.springboardSynopsisColor = ValidStr( secondaryTextColor )
	theme.springboardRuntimeColor = ValidStr( tertiaryTextColor )
	theme.springboardDirectorColor = ValidStr( tertiaryTextColor )
	theme.springboardDirectorLabelColor = ValidStr( tertiaryTextColor )
	theme.springboardActorColor = ValidStr( tertiaryTextColor )

	app.SetTheme( theme )
end sub

function LoadConfig()
	result = []

	result.push( GetLiveStream() )

	opml = CreateObject( "roXMLElement" )
	raw = NWM_UT_GetStringFromURL( "http://roku.jupitercolony.com/opml.xml" ) ' Pull configuration from a remote config file
	if opml.Parse( raw )
		for each category in opml.body.outline
			result.Push( BuildCategory( category ) )
		next
	else ' Fallback if cannot contact server, find file, or parse
		raw = ReadASCIIFile("pkg:/config.opml") ' Pull configuration from a local config file
		if opml.Parse( raw )
			for each category in opml.body.outline
				result.Push( BuildCategory( category ) )
			next
		end if
	end if

	'result.push( GetQualityIcon() )

	return result
end function

function BuildCategory( category )
	result = {
		title:			ValidStr( category@title )
		shortDescriptionLine1:	ValidStr( category@title )
		shortDescriptionLine2:	ValidStr( category@subtitle )
		sdPosterURL:		ValidStr( category@img )
		hdPosterURL:		ValidStr( category@img )
		url:			ValidStr( category@url )
		categories:		[]
	}

	if category@hqfeed <> invalid
		result.url = ValidStr( category@hqfeed )
	end if

	quality = RegRead( "quality" )
	if quality <> invalid AND strtoi( ValidStr( quality ) ) = 1
		if category@lqfeed <> invalid
			result.url = ValidStr( category@lqfeed )
		end if
	end if

	if category.outline.Count() > 0
		for each subCategory in category.outline
			result.categories.Push( BuildCategory( subCategory ) )
		next
	end if

	return result
end function

function GetLiveStream()
	result = {
		screenTarget:		"livestream"
		calendar:		"http://roku.jupitercolony.com/live-calendar.xml"
		live:			true
		title:			"Live Stream"
		shortDescriptionLine1:	"Live Stream"
		shortDescriptionLine2:	"Watch Jupiter Broadcasting Live!"
		sdPosterURL:		"pkg:/images/mm_icon_focus_sd.jpg"
		hdPosterURL:		"pkg:/images/mm_icon_focus_hd.jpg"
		contentType:		"episode"
		streamurls:		["http://videocdn-us.geocdn.scaleengine.net/jblive-iphone/live/jblive.stream/playlist.m3u8"]
		streamformat:		"hls"
		streamqualities:	["SD"]
		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:		[]
	}

	return result
end function

function GetQualityIcon()
	result = {
		screenTarget:		"quality"
		title:			"Quality"
		shortDescriptionLine1:	"Quality"
		shortDescriptionLine2:	"Currently set to HQ"
		sdPosterURL:		"pkg:/images/mm_icon_focus_sd.jpg"
		hdPosterURL:		"pkg:/images/mm_icon_focus_hd.jpg"
		contentType:		"episode"
		categories:		[]
	}

	quality = RegRead( "quality" )
	if( quality <> invalid AND strtoi( ValidStr( quality ) ) = 1 )
		result.shortDescriptionLine2 = "Currently set to LQ (mobile quality)"
	end if

	return result
end function

sub Main()
	LoadTheme()
	categories = LoadConfig()

	if categories.Count() > 1
		ShowPosterScreen( categories )
	else
		ShowEpisodeScreen( categories[0] )
	end if
end sub

sub LoadTheme()
	app = CreateObject( "roAppManager" )
	theme = CreateObject( "roAssociativeArray" )
	theme.OverhangSliceSD = "pkg:/images/Overhang_Slice_SD.png"
	theme.OverhangSliceHD = "pkg:/images/Overhang_Slice_HD.png"
	theme.OverhanglogoHD = "pkg:/images/Logo_Overhang_JB_HD.png"
	theme.OverhanglogoSD = "pkg:/images/Logo_Overhang_JB_SD.png"

	theme.OverhangPrimaryLogoOffsetHD_X = "60"
	theme.OverhangPrimaryLogoOffsetHD_Y = "40"

	theme.OverhangPrimaryLogoOffsetSD_X = "60"
	theme.OverhangPrimaryLogoOffsetSD_Y = "40"

	backgroundColor = "#E0E0E0"
	primaryTextColor = "#333333"
	secondaryTextColor = "#666666"
	tertiaryTextColor = "#999999"

	theme.backgroundColor = ValidStr( backgroundColor )
	theme.breadcrumbTextLeft = ValidStr( primaryTextColor )
	theme.breadcrumbDelimiter = ValidStr( primaryTextColor )
	theme.breadcrumbTextRight = ValidStr( primaryTextColor )

	theme.posterScreenLine1Text = ValidStr( primaryTextColor )
	theme.posterScreenLine2Text = ValidStr( secondaryTextColor )
	theme.episodeSynopsisText = ValidStr( secondaryTextColor )

	theme.springboardTitleText = ValidStr( primaryTextColor )
	theme.springboardSynopsisColor = ValidStr( secondaryTextColor )
	theme.springboardRuntimeColor = ValidStr( tertiaryTextColor )
	theme.springboardDirectorColor = ValidStr( tertiaryTextColor )
	theme.springboardDirectorLabelColor = ValidStr( tertiaryTextColor )
	theme.springboardActorColor = ValidStr( tertiaryTextColor )

	app.SetTheme( theme )
end sub

function LoadConfig()
	result = []

	result.push( GetLiveStream() )

	opml = CreateObject( "roXMLElement" )
	raw = NWM_UT_GetStringFromURL( "http://roku.jupitercolony.com/opml.xml" ) ' Pull configuration from a remote config file
	if opml.Parse( raw )
		for each category in opml.body.outline
			result.Push( BuildCategory( category ) )
		next
	else ' Fallback if cannot contact server, find file, or parse
		raw = ReadASCIIFile("pkg:/config.opml") ' Pull configuration from a local config file
		if opml.Parse( raw )
			for each category in opml.body.outline
				result.Push( BuildCategory( category ) )
			next
		end if
	end if

	'result.push( GetQualityIcon() )

	return result
end function

function BuildCategory( category )
	result = {
		title:			ValidStr( category@title )
		shortDescriptionLine1:	ValidStr( category@title )
		shortDescriptionLine2:	ValidStr( category@subtitle )
		sdPosterURL:		ValidStr( category@img )
		hdPosterURL:		ValidStr( category@img )
		url:			ValidStr( category@url )
		categories:		[]
	}

	if category@hqfeed <> invalid
		result.url = ValidStr( category@hqfeed )
	end if

	quality = RegRead( "quality" )
	if quality <> invalid AND strtoi( ValidStr( quality ) ) = 1
		if category@lqfeed <> invalid
			result.url = ValidStr( category@lqfeed )
		end if
	end if

	if category.outline.Count() > 0
		for each subCategory in category.outline
			result.categories.Push( BuildCategory( subCategory ) )
		next
	end if

	return result
end function

function GetLiveStream()
	result = {
		screenTarget:		"livestream"
		calendar:		"http://roku.jupitercolony.com/live-calendar.xml"
		live:			true
		title:			"Live Stream"
		shortDescriptionLine1:	"Live Stream"
		shortDescriptionLine2:	"Watch Jupiter Broadcasting Live!"
		sdPosterURL:		"pkg:/images/mm_icon_focus_sd.jpg"
		hdPosterURL:		"pkg:/images/mm_icon_focus_hd.jpg"
		contentType:		"episode"
		streamurls:		["http://videocdn-us.geocdn.scaleengine.net/jblive-iphone/live/jblive.stream/playlist.m3u8"]
		streamformat:		"hls"
		streamqualities:	["SD"]
		streambitrates:		[0]
		categories:		[]
	}

	return result
end function

function GetQualityIcon()
	result = {
		screenTarget:		"quality"
		title:			"Quality"
		shortDescriptionLine1:	"Quality"
		shortDescriptionLine2:	"Currently set to HQ"
		sdPosterURL:		"pkg:/images/mm_icon_focus_sd.jpg"
		hdPosterURL:		"pkg:/images/mm_icon_focus_hd.jpg"
		contentType:		"episode"
		categories:		[]
	}

	quality = RegRead( "quality" )
	if( quality <> invalid AND strtoi( ValidStr( quality ) ) = 1 )
		result.shortDescriptionLine2 = "Currently set to LQ (mobile quality)"
	end if

	return result
end function


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

function ShowSpringboardScreen( episodes, selectedEpisode )
	screen = CreateObject( "roSpringboardScreen" )
	screen.SetMessagePort( CreateObject( "roMessagePort" ) )

	'screen.setAdURL( sdAd, hdAd )
	'screen.setAdSelectable( false )

	screen.SetStaticRatingEnabled( false )
	SpringBoardScreen_showContent( screen, episodes[selectedEpisode] )

	while true
		msg = wait( 0, screen.GetMessagePort() )
		if msg <> invalid
			if msg.isScreenClosed()
				exit while
			else if msg.isButtonPressed()
	                	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] )
					screen.Show()
				end if
				if msg.GetIndex() = 2
					episodes[selectedEpisode].PlayStart = 0
					ShowVideoScreen( episodes[selectedEpisode] )
					setButtons( screen, episodes[selectedEpisode] )
					screen.Show()
				end if
			else if msg.isRemoteKeyPressed()
				if msg.GetIndex() = 4 ' LEFT
					if selectedEpisode = 0
						selectedEpisode = episodes.Count() - 1
					else
						selectedEpisode = selectedEpisode - 1
					end if
					SpringBoardScreen_showContent( screen, episodes[selectedEpisode] )
				else if msg.GetIndex() = 5 ' RIGHT
					if selectedEpisode = episodes.Count() - 1
						selectedEpisode = 0
					else
						selectedEpisode = selectedEpisode + 1
					end if
                    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
	if show.sdPosterURL_hq <> invalid and show.sdPosterURL_hq <> ""
		show.sdPosterURL = show.sdPosterURL_hq
		show.sdPosterURL_hq = tmp
	end if
	tmp = show.hdPosterURL
	if show.hdPosterURL_hq <> invalid and show.hdPosterURL_hq <> ""
		show.hdPosterURL = show.hdPosterURL_hq
		show.hdPosterURL_hq = tmp
	end if
end sub

sub SpringBoardScreen_setButtons( screen, episode )
	screen.ClearButtons()
	if RegRead( episode.title ) <> invalid and RegRead( episode.title ).toint() >=30 then
		screen.AddButton( 1, "Resume Playing" )
		screen.AddButton( 2, "Play from Beginning" )
	else
		screen.addbutton( 2,"Play" )
	end if
end sub

function ShowSpringboardScreen( episodes, selectedEpisode )
	screen = CreateObject( "roSpringboardScreen" )
	screen.SetMessagePort( CreateObject( "roMessagePort" ) )

	'screen.setAdURL( sdAd, hdAd )
	'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] )

	while true
		msg = wait( 0, screen.GetMessagePort() )
		if msg <> invalid
			if msg.isScreenClosed()
				exit while
			else if msg.isButtonPressed()
	                	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] )
					screen.Show()
				end if
				if msg.GetIndex() = 2
					episodes[selectedEpisode].PlayStart = 0
					ShowVideoScreen( episodes[selectedEpisode] )
					setButtons( screen, episodes[selectedEpisode] )
					screen.Show()
				end if
			else if msg.isRemoteKeyPressed()
				if msg.GetIndex() = 4 ' LEFT
					if selectedEpisode = 0
						selectedEpisode = episodes.Count() - 1
					else
						selectedEpisode = selectedEpisode - 1
					end if
					screen.SetContent( 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] )
				end if
			end if
		end if
	end while
	return selectedEpisode
end function

sub SpringBoardScreen_swapPoster( show )
	tmp = show.sdPosterURL
	if show.sdPosterURL_hq <> invalid and show.sdPosterURL_hq <> ""
		show.sdPosterURL = show.sdPosterURL_hq
		show.sdPosterURL_hq = tmp
	end if
	tmp = show.hdPosterURL
	if show.hdPosterURL_hq <> invalid and show.hdPosterURL_hq <> ""
		show.hdPosterURL = show.hdPosterURL_hq
		show.hdPosterURL_hq = tmp
	end if
end sub

sub setButtons( screen, episode )
	screen.ClearButtons()
	if RegRead( episode.title ) <> invalid and RegRead( episode.title ).toint() >=30 then
		screen.AddButton( 1, "Resume Playing" )
		screen.AddButton( 2, "Play from Beginning" )
	else
		screen.addbutton( 2,"Play" )
	end if
end sub


Added audio live stream to live stream screen.

function ShowLivestreamScreen( livestream )
	screen = CreateObject( "roSpringboardScreen" )
	screen.SetMessagePort( CreateObject( "roMessagePort" ) )

	'screen.setAdURL( sdAd, hdAd )
	'screen.setAdSelectable( false )

	' Disable star ratings
	screen.SetStaticRatingEnabled( false )
	' Add "Play" button
	Livestream_SetButtons( screen, false )
	' 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
		nextevent = invalid
		now = CreateObject( "roDateTime" )
		now.mark()
		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
			end if
		next
		nestart = strtoi( ValidStr( nextevent.start.GetText() ) )
		nefinish = strtoi( ValidStr( nextevent.finish.GetText() ) )

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

			' 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 less than 10 minutes, add leading zero
				desc = desc + "0"
			end if
			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."

		livestream.shortDescriptionLine2 = desc
		livestream.description = desc
		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()
				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

function ShowLivestreamScreen( livestream )
	screen = CreateObject( "roSpringboardScreen" )
	screen.SetMessagePort( CreateObject( "roMessagePort" ) )

	'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
		nextevent = invalid
		now = CreateObject( "roDateTime" )
		now.mark()
		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
			end if
		next
		nestart = strtoi( ValidStr( nextevent.start.GetText() ) )
		nefinish = strtoi( ValidStr( nextevent.finish.GetText() ) )

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

			' 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 less than 10 minutes, add leading zero
				desc = desc + "0"
			end if
			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."

		livestream.shortDescriptionLine2 = desc
		livestream.description = desc
		screen.SetContent( livestream )
		screen.Show()
	end if

	while true
		msg = wait( 0, screen.GetMessagePort() )
		if msg <> invalid
			if msg.isScreenClosed()
				exit while
			else if msg.isButtonPressed()
				ShowVideoScreen( livestream )
			endif
		end if
	end while

	return selectedEpisode
end function


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

'**********************************************************
' Video Player Example Application - Video Playback
' November 2009
' Copyright (c) 2009 Roku Inc. All Rights Reserved.
'**********************************************************

'***********************************************************
' Create and show the video screen.  The video screen is
' a special full screen video playback component.  It
' handles most of the keypresses automatically and our
' job is primarily to make sure it has the correct data
' at startup. We will receive event back on progress and
' error conditions so it's important to monitor these to
' understand what's going on, especially in the case of errors
'***********************************************************
Function showVideoScreen(episode As Object)
	if type(episode) <> "roAssociativeArray" then
		print "invalid data passed to showVideoScreen"
		return -1
	endif

	port = CreateObject("roMessagePort")
	screen = CreateObject("roVideoScreen")
	screen.SetMessagePort(port)

	screen.SetContent( episode )
	screen.SetPositionNotificationPeriod( 15 )
	screen.Show()

	while true
		msg = wait(0, port)
		if type(msg) = "roVideoScreenEvent" then
			if msg.isScreenClosed()
				print "Screen closed"
				exit while
			else if msg.isRequestFailed()
				print "Video request failure: "; msg.GetIndex(); " " msg.GetData()
			else if msg.isStatusMessage()
				print "Video status: "; msg.GetIndex(); " " msg.GetData()
			else if msg.isButtonPressed()
				print "Button pressed: "; msg.GetIndex(); " " msg.GetData()
			else if msg.isPlaybackPosition() then
				nowpos = msg.GetIndex()
				RegWrite( episode.title, nowpos.toStr() )
				print "Marked progress: "; msg.GetIndex()
			else if msg.isFullResult() then
				RegDelete( episode.title )
			else
				print "Unexpected event type: "; msg.GetType()
			end if
		else
			print "Unexpected message class: "; type(msg)
		end if
	end while
End Function

'**********************************************************
' Video Player Example Application - Video Playback
' November 2009
' Copyright (c) 2009 Roku Inc. All Rights Reserved.
'**********************************************************

'***********************************************************
' Create and show the video screen.  The video screen is
' a special full screen video playback component.  It
' handles most of the keypresses automatically and our
' job is primarily to make sure it has the correct data
' at startup. We will receive event back on progress and
' error conditions so it's important to monitor these to
' understand what's going on, especially in the case of errors
'***********************************************************
Function showVideoScreen(episode As Object)
	if type(episode) <> "roAssociativeArray" then
		print "invalid data passed to showVideoScreen"
		return -1
	endif

	port = CreateObject("roMessagePort")
	screen = CreateObject("roVideoScreen")
	screen.SetMessagePort(port)

	screen.SetContent( episode )
	screen.SetPositionNotificationPeriod( 3 ) 'Fire isPlaybackPosition every 3 seconds
	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
			else if msg.isRequestFailed()
				print "Video request failure: "; msg.GetIndex(); " " msg.GetData()
			else if msg.isStatusMessage()
				print "Video status: "; msg.GetIndex(); " " msg.GetData()
			else if msg.isButtonPressed()
				print "Button pressed: "; msg.GetIndex(); " " msg.GetData()
			else if msg.isPlaybackPosition() then
		                nowpos = msg.GetIndex()
		                RegWrite( episode.title, nowpos.toStr() )
				print "Marked progress: "; msg.GetIndex()
			else if msg.isFullResult() then
		                RegDelete( episode.title )
			else
				print "Unexpected event type: "; msg.GetType()
			end if
		else
			print "Unexpected message class: "; type(msg)
		end if
	end while
End Function


Function setButtons has been renamed.

Function setButtons has been renamed.

function ShowSpringboardScreen( episodes, selectedEpisode )
	screen = CreateObject( "roSpringboardScreen" )
	screen.SetMessagePort( CreateObject( "roMessagePort" ) )

	'screen.setAdURL( sdAd, hdAd )
	'screen.setAdSelectable( false )

	screen.SetStaticRatingEnabled( false )
	SpringBoardScreen_showContent( screen, episodes[selectedEpisode] )

	while true
		msg = wait( 0, screen.GetMessagePort() )
		if msg <> invalid
			if msg.isScreenClosed()
				exit while
			else if msg.isButtonPressed()
				if msg.GetIndex() = 1
					PlayStart = RegRead( episodes[selectedEpisode].title )
					if PlayStart <> invalid then
						episodes[selectedEpisode].PlayStart = PlayStart.ToInt()
					end if
					ShowVideoScreen( episodes[selectedEpisode] )
					SpringBoardScreen_setButtons( screen, episodes[selectedEpisode] )
					screen.Show()
				end if
				if msg.GetIndex() = 2
					episodes[selectedEpisode].PlayStart = 0
					ShowVideoScreen( episodes[selectedEpisode] )
					SpringBoardScreen_setButtons( screen, episodes[selectedEpisode] )
					screen.Show()
				end if
			else if msg.isRemoteKeyPressed()
				if msg.GetIndex() = 4 ' LEFT
					if selectedEpisode = 0
						selectedEpisode = episodes.Count() - 1
					else
						selectedEpisode = selectedEpisode - 1
					end if
					SpringBoardScreen_showContent( screen, episodes[selectedEpisode] )
				else if msg.GetIndex() = 5 ' RIGHT
					if selectedEpisode = episodes.Count() - 1
						selectedEpisode = 0
					else
						selectedEpisode = selectedEpisode + 1
					end if
                    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
	if show.sdPosterURL_hq <> invalid and show.sdPosterURL_hq <> ""
		show.sdPosterURL = show.sdPosterURL_hq
		show.sdPosterURL_hq = tmp
	end if
	tmp = show.hdPosterURL
	if show.hdPosterURL_hq <> invalid and show.hdPosterURL_hq <> ""
		show.hdPosterURL = show.hdPosterURL_hq
		show.hdPosterURL_hq = tmp
	end if
end sub

sub SpringBoardScreen_setButtons( screen, episode )
	screen.ClearButtons()
	if RegRead( episode.title ) <> invalid and RegRead( episode.title ).toint() >=30 then
		screen.AddButton( 1, "Resume Playing" )
		screen.AddButton( 2, "Play from Beginning" )
	else
		screen.addbutton( 2,"Play" )
	end if
end sub

function ShowSpringboardScreen( episodes, selectedEpisode )
	screen = CreateObject( "roSpringboardScreen" )
	screen.SetMessagePort( CreateObject( "roMessagePort" ) )

	'screen.setAdURL( sdAd, hdAd )
	'screen.setAdSelectable( false )

	screen.SetStaticRatingEnabled( false )
	SpringBoardScreen_showContent( screen, episodes[selectedEpisode] )

	while true
		msg = wait( 0, screen.GetMessagePort() )
		if msg <> invalid
			if msg.isScreenClosed()
				exit while
			else if msg.isButtonPressed()
	                	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] )
					screen.Show()
				end if
				if msg.GetIndex() = 2
					episodes[selectedEpisode].PlayStart = 0
					ShowVideoScreen( episodes[selectedEpisode] )
					setButtons( screen, episodes[selectedEpisode] )
					screen.Show()
				end if
			else if msg.isRemoteKeyPressed()
				if msg.GetIndex() = 4 ' LEFT
					if selectedEpisode = 0
						selectedEpisode = episodes.Count() - 1
					else
						selectedEpisode = selectedEpisode - 1
					end if
					SpringBoardScreen_showContent( screen, episodes[selectedEpisode] )
				else if msg.GetIndex() = 5 ' RIGHT
					if selectedEpisode = episodes.Count() - 1
						selectedEpisode = 0
					else
						selectedEpisode = selectedEpisode + 1
					end if
                    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
	if show.sdPosterURL_hq <> invalid and show.sdPosterURL_hq <> ""
		show.sdPosterURL = show.sdPosterURL_hq
		show.sdPosterURL_hq = tmp
	end if
	tmp = show.hdPosterURL
	if show.hdPosterURL_hq <> invalid and show.hdPosterURL_hq <> ""
		show.hdPosterURL = show.hdPosterURL_hq
		show.hdPosterURL_hq = tmp
	end if
end sub

sub SpringBoardScreen_setButtons( screen, episode )
	screen.ClearButtons()
	if RegRead( episode.title ) <> invalid and RegRead( episode.title ).toint() >=30 then
		screen.AddButton( 1, "Resume Playing" )
		screen.AddButton( 2, "Play from Beginning" )
	else
		screen.addbutton( 2,"Play" )
	end if
end sub


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

Function CreateAudioPlayer( port As Object ) As Object
	audioPlayer = CreateObject("roAssociativeArray")
	audioPlayer.player = CreateObject("roAudioPlayer")
	audioPlayer.playing = false

	audioPlayer.IsPlaying = AudioPlayer_IsPlaying
	audioPlayer.Stop = AudioPlayer_Stop
	audioPlayer.Play = AudioPlayer_Play
	audioPlayer.SetStream = AudioPlayer_SetStream

	return audioPlayer
end Function

Function AudioPlayer_IsPlaying()
	return m.playing
end Function

Function AudioPlayer_Stop()
	m.player.Stop()
	m.playing = false
end Function

Function AudioPlayer_Play()
	m.player.Play()
	m.playing = true
end Function

Function AudioPlayer_SetStream( audioStream As Object )
	m.player.AddContent( audioStream )
	m.player.SetLoop( false )
end Function