diff --git a/components/ItemGrid/ItemGrid.brs b/components/ItemGrid/ItemGrid.brs index a1d57ab51..e740c7aac 100644 --- a/components/ItemGrid/ItemGrid.brs +++ b/components/ItemGrid/ItemGrid.brs @@ -725,6 +725,7 @@ sub showTVGuide() m.tvGuide.filter = m.filter m.tvGuide.searchTerm = m.voiceBox.text m.top.appendChild(m.tvGuide) + m.scheduleGrid = m.top.findNode("scheduleGrid") m.tvGuide.lastFocus.setFocus(true) end sub @@ -742,6 +743,18 @@ sub onChannelFocused(msg) m.channelFocused = node.focusedChannel end sub +'Returns Focused Item +function getItemFocused() + if m.itemGrid.isinFocusChain() and isValid(m.itemGrid.itemFocused) + return m.itemGrid.content.getChild(m.itemGrid.itemFocused) + else if m.genreList.isinFocusChain() and isValid(m.genreList.rowItemFocused) + return m.genreList.content.getChild(m.genreList.rowItemFocused[0]).getChild(m.genreList.rowItemFocused[1]) + else if m.scheduleGrid.isinFocusChain() and isValid(m.scheduleGrid.itemFocused) + return m.scheduleGrid.content.getChild(m.scheduleGrid.itemFocused) + end if + return invalid +end function + function onKeyEvent(key as string, press as boolean) as boolean if not press then return false @@ -788,11 +801,11 @@ function onKeyEvent(key as string, press as boolean) as boolean m.loadItemsTask.control = "stop" return true end if - else if key = "play" or key = "OK" + else if key = "play" markupGrid = m.top.findNode("itemGrid") - itemToPlay = markupGrid.content.getChild(markupGrid.itemFocused) + itemToPlay = getItemFocused() - if itemToPlay <> invalid and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode") + if itemToPlay <> invalid m.top.quickPlayNode = itemToPlay return true else if itemToPlay <> invalid and itemToPlay.type = "Photo" diff --git a/components/ItemGrid/MovieLibraryView.brs b/components/ItemGrid/MovieLibraryView.brs index e36ae141e..817573c62 100644 --- a/components/ItemGrid/MovieLibraryView.brs +++ b/components/ItemGrid/MovieLibraryView.brs @@ -708,7 +708,12 @@ end sub ' 'Returns Focused Item function getItemFocused() - return m.itemGrid.content.getChild(m.itemGrid.itemFocused) + if m.itemGrid.isinFocusChain() and isValid(m.itemGrid.itemFocused) + return m.itemGrid.content.getChild(m.itemGrid.itemFocused) + else if m.genreList.isinFocusChain() and isValid(m.genreList.rowItemFocused) + return m.genreList.content.getChild(m.genreList.rowItemFocused[0]).getChild(m.genreList.rowItemFocused[1]) + end if + return invalid end function ' @@ -870,11 +875,10 @@ function onKeyEvent(key as string, press as boolean) as boolean m.loadItemsTask.control = "stop" return true end if - else if key = "play" or key = "OK" - + else if key = "play" itemToPlay = getItemFocused() - if itemToPlay <> invalid and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode") + if itemToPlay <> invalid m.top.quickPlayNode = itemToPlay return true end if diff --git a/components/ItemGrid/MusicLibraryView.brs b/components/ItemGrid/MusicLibraryView.brs index b11b8dacc..1573a016e 100644 --- a/components/ItemGrid/MusicLibraryView.brs +++ b/components/ItemGrid/MusicLibraryView.brs @@ -573,7 +573,12 @@ end sub ' 'Returns Focused Item function getItemFocused() - return m.itemGrid.content.getChild(m.itemGrid.itemFocused) + if m.itemGrid.isinFocusChain() and isValid(m.itemGrid.itemFocused) + return m.itemGrid.content.getChild(m.itemGrid.itemFocused) + else if m.genreList.isinFocusChain() and isValid(m.genreList.itemFocused) + return m.genreList.content.getChild(m.genreList.itemFocused) + end if + return invalid end function ' @@ -751,7 +756,6 @@ function onKeyEvent(key as string, press as boolean) as boolean alpha.setFocus(true) return true end if - else if key = "right" and m.Alpha.isinFocusChain() m.top.alphaActive = false m.Alpha.setFocus(false) @@ -761,14 +765,12 @@ function onKeyEvent(key as string, press as boolean) as boolean m.genreList.setFocus(m.genreList.opacity = 1) return true - else if key = "replay" and m.itemGrid.isinFocusChain() if m.resetGrid = true m.itemGrid.animateToItem = 0 else m.itemGrid.jumpToItem = 0 end if - else if key = "replay" and m.genreList.isinFocusChain() if m.resetGrid = true m.genreList.animateToItem = 0 @@ -776,6 +778,12 @@ function onKeyEvent(key as string, press as boolean) as boolean m.genreList.jumpToItem = 0 end if return true + else if key = "play" + itemToPlay = getItemFocused() + if itemToPlay <> invalid + m.top.quickPlayNode = itemToPlay + return true + end if end if if key = "replay" diff --git a/components/data/HomeData.brs b/components/data/HomeData.brs index 4266cc709..bba8aeca9 100644 --- a/components/data/HomeData.brs +++ b/components/data/HomeData.brs @@ -31,7 +31,7 @@ sub setData() m.top.iconUrl = "pkg:/images/media_type_icons/folder_white.png" end if - else if datum.type = "Episode" + else if datum.type = "Episode" or datum.type = "MusicVideo" m.top.isWatched = datum.UserData.Played imgParams = {} @@ -72,32 +72,7 @@ sub setData() m.top.widePosterUrl = ImageURL(datum.Id, "Backdrop", imgParams) end if - else if datum.type = "Movie" - m.top.isWatched = datum.UserData.Played - - imgParams = {} - imgParams.Append({ "maxHeight": 261 }) - imgParams.Append({ "maxWidth": 175 }) - - if datum.ImageTags.Primary <> invalid - param = { "Tag": datum.ImageTags.Primary } - imgParams.Append(param) - end if - - m.top.posterURL = ImageURL(datum.id, "Primary", imgParams) - - ' For wide image, use backdrop - imgParams["maxWidth"] = 464 - - if datum.ImageTags <> invalid and datum.imageTags.Thumb <> invalid - imgParams["Tag"] = datum.imageTags.Thumb - m.top.thumbnailUrl = ImageURL(datum.Id, "Thumb", imgParams) - else if datum.BackdropImageTags[0] <> invalid - imgParams["Tag"] = datum.BackdropImageTags[0] - m.top.thumbnailUrl = ImageURL(datum.id, "Backdrop", imgParams) - end if - - else if datum.type = "Video" + else if datum.type = "Movie" or datum.type = "Video" m.top.isWatched = datum.UserData.Played imgParams = {} @@ -126,12 +101,10 @@ sub setData() m.top.thumbnailURL = ImageURL(datum.id, "Primary", params) m.top.widePosterUrl = m.top.thumbnailURL m.top.posterUrl = m.top.thumbnailURL - else if datum.type = "TvChannel" or datum.type = "Channel" params = { "Tag": datum.ImageTags.Primary, "maxHeight": 261, "maxWidth": 464 } m.top.thumbnailURL = ImageURL(datum.id, "Primary", params) m.top.widePosterUrl = m.top.thumbnailURL m.top.iconUrl = "pkg:/images/media_type_icons/live_tv_white.png" end if - end sub diff --git a/components/extras/ExtrasRowList.brs b/components/extras/ExtrasRowList.brs index 230622cdd..c008795aa 100644 --- a/components/extras/ExtrasRowList.brs +++ b/components/extras/ExtrasRowList.brs @@ -3,6 +3,7 @@ sub init() updateSize() m.top.rowFocusAnimationStyle = "fixedFocus" m.top.observeField("rowItemSelected", "onRowItemSelected") + m.top.observeField("rowItemFocused", "onRowItemFocused") ' Set up all Tasks m.LoadPeopleTask = CreateObject("roSGNode", "LoadItemsTask") @@ -207,3 +208,7 @@ end sub sub onRowItemSelected() m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1]) end sub + +sub onRowItemFocused() + m.top.focusedItem = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1]) +end sub diff --git a/components/extras/ExtrasRowList.xml b/components/extras/ExtrasRowList.xml index 8245dd2d8..55f96a421 100644 --- a/components/extras/ExtrasRowList.xml +++ b/components/extras/ExtrasRowList.xml @@ -4,6 +4,7 @@ + diff --git a/components/home/HomeItem.brs b/components/home/HomeItem.brs index 8fb835290..29c2f1342 100644 --- a/components/home/HomeItem.brs +++ b/components/home/HomeItem.brs @@ -30,6 +30,7 @@ end sub sub itemContentChanged() itemData = m.top.itemContent if itemData = invalid then return + itemData.Title = itemData.name ' Temporarily required while we move from "HomeItem" to "JFContentItem" m.itemPoster.width = itemData.imageWidth @@ -135,7 +136,7 @@ sub itemContentChanged() return end if - if itemData.type = "Movie" + if itemData.type = "Movie" or itemData.type = "MusicVideo" m.itemText.text = itemData.name if itemData.PlayedPercentage > 0 diff --git a/components/home/HomeRows.brs b/components/home/HomeRows.brs index 68a4a9ecd..609c42b89 100644 --- a/components/home/HomeRows.brs +++ b/components/home/HomeRows.brs @@ -522,21 +522,20 @@ sub itemSelected() end sub function onKeyEvent(key as string, press as boolean) as boolean - handled = false if press if key = "play" + print "play was pressed from homerow" itemToPlay = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1]) - if isValid(itemToPlay) and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode") + if isValid(itemToPlay) m.top.quickPlayNode = itemToPlay end if - handled = true - end if - - if key = "replay" + return true + else if key = "replay" m.top.jumpToRowItem = [m.top.rowItemFocused[0], 0] + return true end if end if - return handled + return false end function function filterNodeArray(nodeArray as object, nodeKey as string, excludeArray as object) as object diff --git a/components/home/HomeRows.xml b/components/home/HomeRows.xml index 5c0dcfc03..77d91aeb6 100644 --- a/components/home/HomeRows.xml +++ b/components/home/HomeRows.xml @@ -2,7 +2,7 @@ - + diff --git a/components/home/LoadItemsTask.brs b/components/home/LoadItemsTask.brs index 79bb2d387..1ea788439 100644 --- a/components/home/LoadItemsTask.brs +++ b/components/home/LoadItemsTask.brs @@ -137,7 +137,8 @@ sub loadItems() if isValid(data) and isValid(data.Items) for each item in data.Items ' Skip Books for now as we don't support it (issue #558) - if item.Type <> "Book" + ' also skip songs since there is limited space + if not (item.Type = "Book" or item.Type = "Audio") tmp = CreateObject("roSGNode", "HomeData") params = {} diff --git a/components/manager/QueueManager.brs b/components/manager/QueueManager.brs index ca9f30fa2..4a5b14b9e 100644 --- a/components/manager/QueueManager.brs +++ b/components/manager/QueueManager.brs @@ -11,6 +11,7 @@ sub init() m.queue = [] m.originalQueue = [] m.queueTypes = [] + m.isPlaying = false ' Preroll videos only play if user has cinema mode setting enabled m.isPrerollActive = m.global.session.user.settings["playback.cinemamode"] m.position = 0 @@ -19,6 +20,7 @@ end sub ' Clear all content from play queue sub clear() + m.isPlaying = false m.queue = [] m.queueTypes = [] m.isPrerollActive = m.global.session.user.settings["playback.cinemamode"] @@ -111,6 +113,7 @@ end function ' Play items in queue sub playQueue() + m.isPlaying = true nextItem = getCurrentItem() if not isValid(nextItem) then return @@ -122,11 +125,21 @@ sub playQueue() return end if + if nextItemMediaType = "musicvideo" + CreateVideoPlayerView() + return + end if + if nextItemMediaType = "video" CreateVideoPlayerView() return end if + if nextItemMediaType = "movie" + CreateVideoPlayerView() + return + end if + if nextItemMediaType = "episode" CreateVideoPlayerView() return @@ -196,21 +209,25 @@ end function sub shuffleQueueItems() ' By calling getQueue 2 different ways, Roku avoids needing to do a deep copy m.originalQueue = m.global.queueManager.callFunc("getQueue") - songIDArray = getQueue() - - ' Move the currently playing song to the front of the queue - temp = top() - songIDArray[0] = getCurrentItem() - songIDArray[getPosition()] = temp - - for i = 1 to songIDArray.count() - 1 - j = Rnd(songIDArray.count() - 1) - temp = songIDArray[i] - songIDArray[i] = songIDArray[j] - songIDArray[j] = temp - end for + itemIDArray = getQueue() + temp = invalid + + if m.isPlaying + ' Save the currently playing item + temp = getCurrentItem() + ' remove currently playing item from itemIDArray + itemIDArray.Delete(m.position) + end if + + ' shuffle all items + itemIDArray = shuffleArray(itemIDArray) + + if m.isPlaying + ' Put currently playing item in front of itemIDArray + itemIDArray.Unshift(temp) + end if - set(songIDArray) + set(itemIDArray) end sub ' Return the fitst item in the play queue diff --git a/components/movies/MovieDetails.brs b/components/movies/MovieDetails.brs index 913ccd796..123a4a939 100644 --- a/components/movies/MovieDetails.brs +++ b/components/movies/MovieDetails.brs @@ -385,6 +385,12 @@ function onKeyEvent(key as string, press as boolean) as boolean audioOptionsClosed() return true end if + else if key = "play" and m.extrasGrid.hasFocus() + print "Play was pressed from the movie details extras slider" + if m.extrasGrid.focusedItem <> invalid + m.top.quickPlayNode = m.extrasGrid.focusedItem + return true + end if end if return false end function diff --git a/components/movies/MovieDetails.xml b/components/movies/MovieDetails.xml index cb850da0f..01222f140 100644 --- a/components/movies/MovieDetails.xml +++ b/components/movies/MovieDetails.xml @@ -50,5 +50,6 @@ + \ No newline at end of file diff --git a/components/music/ArtistView.brs b/components/music/ArtistView.brs index 38336ecf2..4d9365690 100644 --- a/components/music/ArtistView.brs +++ b/components/music/ArtistView.brs @@ -313,5 +313,21 @@ function onKeyEvent(key as string, press as boolean) as boolean end if end if + if key = "play" + print "play button pressed from ArtistView" + itemToPlay = invalid + + if isValid(m.albums) and m.albums.isInFocusChain() + itemToPlay = m.albums.MusicArtistAlbumData.items[m.albums.itemFocused] + else if isValid(m.appearsOn) and m.appearsOn.isInFocusChain() + itemToPlay = m.appearsOn.MusicArtistAlbumData.items[m.appearsOn.itemFocused] + end if + + if isValid(itemToPlay) + m.top.quickPlayNode = itemToPlay + return true + end if + end if + return false end function diff --git a/components/music/ArtistView.xml b/components/music/ArtistView.xml index c8b9be042..64f3d8ebc 100644 --- a/components/music/ArtistView.xml +++ b/components/music/ArtistView.xml @@ -54,5 +54,6 @@ + \ No newline at end of file diff --git a/components/tvshows/TVEpisodes.xml b/components/tvshows/TVEpisodes.xml index c87d404d9..ec2dd9520 100644 --- a/components/tvshows/TVEpisodes.xml +++ b/components/tvshows/TVEpisodes.xml @@ -12,7 +12,7 @@ - + diff --git a/components/tvshows/TVShowDetails.brs b/components/tvshows/TVShowDetails.brs index e5e1ead44..a8006df43 100644 --- a/components/tvshows/TVShowDetails.brs +++ b/components/tvshows/TVShowDetails.brs @@ -11,6 +11,7 @@ sub init() m.getShuffleEpisodesTask = createObject("roSGNode", "getShuffleEpisodesTask") m.Shuffle = m.top.findNode("Shuffle") m.extrasSlider.visible = true + m.seasons = m.top.findNode("seasons") end sub sub itemContentChanged() @@ -223,6 +224,20 @@ function onKeyEvent(key as string, press as boolean) as boolean else if key = "up" and m.Shuffle.hasFocus() overview.setFocus(true) return true + else if key = "play" and m.seasons.hasFocus() + print "play was pressed from the seasons row" + if isValid(m.seasons.TVSeasonData) and isValid(m.seasons.TVSeasonData.Items) + itemFocused = m.seasons.rowItemFocused + m.top.quickPlayNode = m.seasons.TVSeasonData.Items[itemFocused[1]] + return true + end if + else if key = "play" and m.extrasSlider.isInFocusChain() + print "play was pressed from the extras grid" + extrasGrid = m.top.findNode("extrasGrid") + if extrasGrid.focusedItem <> invalid + m.top.quickPlayNode = extrasGrid.focusedItem + return true + end if end if return false diff --git a/components/tvshows/TVShowDetails.xml b/components/tvshows/TVShowDetails.xml index 4c42dfd52..197ee81cf 100644 --- a/components/tvshows/TVShowDetails.xml +++ b/components/tvshows/TVShowDetails.xml @@ -32,5 +32,6 @@ + \ No newline at end of file diff --git a/source/Main.brs b/source/Main.brs index 347f4b632..f2d0ecf05 100644 --- a/source/Main.brs +++ b/source/Main.brs @@ -125,48 +125,89 @@ sub Main (args as dynamic) as void group.setFocus(true) end if else if isNodeEvent(msg, "quickPlayNode") + ' measure processing time + timeSpan = CreateObject("roTimespan") + + startMediaLoadingSpinner() + group = sceneManager.callFunc("getActiveScene") reportingNode = msg.getRoSGNode() - itemNode = reportingNode.quickPlayNode + itemNode = invalid + if isValid(reportingNode) + itemNode = reportingNode.quickPlayNode + reportingNodeType = reportingNode.subtype() + print "Quick Play reporting node type=", reportingNodeType + ' prevent double fire bug + if isValid(reportingNodeType) and (reportingNodeType = "Home" or reportingNodeType = "TVEpisodes") + reportingNode.quickPlayNode = invalid + end if + end if + print "Quick Play started. itemNode=", itemNode + ' if itemNode.json <> invalid + ' print "itemNode.json=", itemNode.json + ' end if if isValid(itemNode) and isValid(itemNode.id) and itemNode.id <> "" - if itemNode.type = "Episode" or itemNode.type = "Movie" or itemNode.type = "Video" - if isValid(itemNode.selectedVideoStreamId) - itemNode.id = itemNode.selectedVideoStreamId - end if - - audio_stream_idx = 0 - if isValid(itemNode.selectedAudioStreamIndex) and itemNode.selectedAudioStreamIndex > 0 - audio_stream_idx = itemNode.selectedAudioStreamIndex - end if - - itemNode.selectedAudioStreamIndex = audio_stream_idx - - playbackPosition = 0 - - ' Display playback options dialog - if isValid(itemNode.json) and isValid(itemNode.json.userdata) and isValid(itemNode.json.userdata.PlaybackPositionTicks) - playbackPosition = itemNode.json.userdata.PlaybackPositionTicks - end if - - if playbackPosition > 0 - m.global.queueManager.callFunc("hold", itemNode) - playbackOptionDialog(playbackPosition, itemNode.json) - else - m.global.queueManager.callFunc("clear") - m.global.queueManager.callFunc("push", itemNode) - m.global.queueManager.callFunc("playQueue") + ' make sure there is a type and convert type to lowercase + itemType = invalid + if isValid(itemNode.type) and itemNode.type <> "" + itemType = Lcase(itemNode.type) + else + ' grab type from json and convert to lowercase + if isValid(itemNode.json) and isValid(itemNode.json.type) + itemType = Lcase(itemNode.json.type) end if - - ' Prevent quick play node from double firing - reportingNode.quickPlayNode = invalid - - if LCase(group.subtype()) = "tvepisodes" - if isValid(group.lastFocus) - group.lastFocus.setFocus(true) + end if + print "Quick Play itemNode type=", itemType + + ' can't play the item without knowing what type it is + if isValid(itemType) + m.global.queueManager.callFunc("clear") ' empty queue/playlist + m.global.queueManager.callFunc("resetShuffle") ' turn shuffle off + + if itemType = "episode" or itemType = "movie" or itemType = "video" + quickplay.video(itemNode) + ' restore focus + if LCase(group.subtype()) = "tvepisodes" + if isValid(group.lastFocus) + group.lastFocus.setFocus(true) + end if end if + else if itemType = "audio" + quickplay.audio(itemNode) + else if itemType = "musicalbum" + quickplay.album(itemNode) + else if itemType = "musicartist" + quickplay.artist(itemNode) + else if itemType = "series" + quickplay.series(itemNode) + else if itemType = "season" + quickplay.season(itemNode) + else if itemType = "boxset" + quickplay.boxset(itemNode) + else if itemType = "collectionfolder" + quickplay.collectionFolder(itemNode) + else if itemType = "playlist" + quickplay.playlist(itemNode) + else if itemType = "userview" + quickplay.userView(itemNode) + else if itemType = "folder" + quickplay.folder(itemNode) + else if itemType = "musicvideo" + quickplay.musicVideo(itemNode) + else if itemType = "person" + quickplay.person(itemNode) + else if itemType = "tvchannel" + quickplay.tvChannel(itemNode) + else if itemType = "program" + quickplay.program(itemNode) end if + + m.global.queueManager.callFunc("playQueue") end if end if + stopLoadingSpinner() + elapsed = timeSpan.TotalMilliseconds() / 1000 + print "Quick Play finished loading in " + elapsed.toStr() + " seconds." else if isNodeEvent(msg, "selectedItem") ' If you select a library from ANYWHERE, follow this flow selectedItem = msg.getData() @@ -259,6 +300,8 @@ sub Main (args as dynamic) as void end if else if selectedItemType = "MusicAlbum" group = CreateAlbumView(selectedItem.json) + else if selectedItemType = "MusicVideo" + group = CreateMovieDetailsGroup(selectedItem) else if selectedItemType = "Playlist" group = CreatePlaylistView(selectedItem.json) else if selectedItemType = "Audio" @@ -397,6 +440,8 @@ sub Main (args as dynamic) as void group = CreateArtistView(node.json) else if node.type = "MusicAlbum" group = CreateAlbumView(node.json) + else if node.type = "MusicVideo" + group = CreateMovieDetailsGroup(node) else if node.type = "Audio" m.global.queueManager.callFunc("clear") m.global.queueManager.callFunc("resetShuffle") @@ -621,6 +666,7 @@ sub Main (args as dynamic) as void ' - "low" means that the general memory is below acceptable levels but not critical ' - "critical" means that general memory are at dangerously low level and that the OS may force terminate the application print "event.generalMemoryLevel = ", event.generalMemoryLevel + session.Update("memoreyLevel", event.generalMemoryLevel) else if isValid(event.audioCodecCapabilityChanged) ' The audio codec capability has changed if true. print "event.audioCodecCapabilityChanged = ", event.audioCodecCapabilityChanged diff --git a/source/ShowScenes.brs b/source/ShowScenes.brs index 28d38b845..ebac0c585 100644 --- a/source/ShowScenes.brs +++ b/source/ShowScenes.brs @@ -598,6 +598,7 @@ function CreateMovieDetailsGroup(movie as object) as dynamic end if ' start building MovieDetails view group = CreateObject("roSGNode", "MovieDetails") + group.observeField("quickPlayNode", m.port) group.overhangTitle = movie.title group.optionsAvailable = false group.trailerAvailable = false @@ -651,6 +652,7 @@ function CreateSeriesDetailsGroup(seriesID as string) as dynamic group.seasonData = seasonData ' watch for button presses group.observeField("seasonSelected", m.port) + group.observeField("quickPlayNode", m.port) ' setup and load series extras extras = group.findNode("extrasGrid") extras.observeField("selectedItem", m.port) @@ -703,6 +705,7 @@ function CreateArtistView(artist as object) as dynamic group.observeField("appearsOnSelected", m.port) end if + group.observeField("quickPlayNode", m.port) m.global.sceneManager.callFunc("pushScene", group) return group @@ -821,6 +824,7 @@ function CreateItemGrid(libraryItem as object) as dynamic group.parentItem = libraryItem group.optionsAvailable = true group.observeField("selectedItem", m.port) + group.observeField("quickPlayNode", m.port) return group end function @@ -832,6 +836,7 @@ function CreateMovieLibraryView(libraryItem as object) as dynamic group.parentItem = libraryItem group.optionsAvailable = true group.observeField("selectedItem", m.port) + group.observeField("quickPlayNode", m.port) return group end function @@ -843,6 +848,7 @@ function CreateMusicLibraryView(libraryItem as object) as dynamic group.parentItem = libraryItem group.optionsAvailable = true group.observeField("selectedItem", m.port) + group.observeField("quickPlayNode", m.port) return group end function diff --git a/source/api/Items.brs b/source/api/Items.brs index 699c8de6d..90f77458d 100644 --- a/source/api/Items.brs +++ b/source/api/Items.brs @@ -225,15 +225,20 @@ function AppearsOnList(id as string) end function ' Get list of songs belonging to an artist -function GetSongsByArtist(id as string) +function GetSongsByArtist(id as string, params = {} as object) url = Substitute("Users/{0}/Items", m.global.session.user.id) - resp = APIRequest(url, { + paramArray = { "AlbumArtistIds": id, "includeitemtypes": "Audio", "sortBy": "SortName", "Recursive": true - }) + } + ' overwrite defaults with the params provided + for each param in params + paramArray.AddReplace(param, params[param]) + end for + resp = APIRequest(url, paramArray) data = getJson(resp) results = [] @@ -410,7 +415,7 @@ function TVSeasons(id as string) as dynamic results = [] for each item in data.Items imgParams = { "AddPlayedIndicator": item.UserData.Played } - tmp = CreateObject("roSGNode", "TVEpisodeData") + tmp = CreateObject("roSGNode", "TVSeasonData") tmp.image = PosterImage(item.id, imgParams) tmp.json = item results.push(tmp) diff --git a/source/api/sdk.bs b/source/api/sdk.bs index 38ef4929a..9cd713f91 100644 --- a/source/api/sdk.bs +++ b/source/api/sdk.bs @@ -1200,8 +1200,8 @@ namespace api end function ' Gets the original items of a playlist. - function GetItems(id as string, params = {} as object) - req = APIRequest(Substitute("/playlists/{0}/items", id), params) + function GetItems(playlistID as string, params = {} as object) + req = APIRequest(Substitute("/playlists/{0}/items", playlistID), params) return getJson(req) end function diff --git a/source/utils/misc.brs b/source/utils/misc.brs index f91b8b16c..4138a0c9a 100644 --- a/source/utils/misc.brs +++ b/source/utils/misc.brs @@ -392,3 +392,13 @@ function arrayHasValue(arr as object, value as dynamic) as boolean end for return false end function + +' Takes an array of data, shuffles the order, then returns the array +' uses the Fisher-Yates shuffling algorithm +function shuffleArray(array as object) as object + for i = array.count() - 1 to 1 step -1 + j = Rnd(i + 1) - 1 + t = array[i] : array[i] = array[j] : array[j] = t + end for + return array +end function diff --git a/source/utils/quickplay.bs b/source/utils/quickplay.bs new file mode 100644 index 000000000..794d8ef67 --- /dev/null +++ b/source/utils/quickplay.bs @@ -0,0 +1,585 @@ +' All of the Quick Play logic seperated by media type +namespace quickplay + + ' Takes an array of items and adds to global queue. + ' Also shuffles the playlist if asked + sub pushToQueue(queueArray as object, shufflePlay = false as boolean) + if isValidAndNotEmpty(queueArray) + ' load everything + for each item in queueArray + m.global.queueManager.callFunc("push", item) + end for + ' shuffle the playlist if asked + if shufflePlay and m.global.queueManager.callFunc("getCount") > 1 + m.global.queueManager.callFunc("toggleShuffle") + end if + end if + end sub + + ' A single video file. + sub video(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) or not isValid(itemNode.json) then return + + ' attempt to play video file. resume if possible + if isValid(itemNode.selectedVideoStreamId) + itemNode.id = itemNode.selectedVideoStreamId + end if + + audio_stream_idx = 0 + if isValid(itemNode.selectedAudioStreamIndex) and itemNode.selectedAudioStreamIndex > 0 + audio_stream_idx = itemNode.selectedAudioStreamIndex + end if + itemNode.selectedAudioStreamIndex = audio_stream_idx + + playbackPosition = 0 + if isValid(itemNode.json.userdata) and isValid(itemNode.json.userdata.PlaybackPositionTicks) + playbackPosition = itemNode.json.userdata.PlaybackPositionTicks + end if + itemNode.startingPoint = playbackPosition + + m.global.queueManager.callFunc("push", itemNode) + end sub + + ' A single audio file. + sub audio(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + + m.global.queueManager.callFunc("push", itemNode) + end sub + + ' A single music video file. + sub musicVideo(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) or not isValid(itemNode.json) then return + + m.global.queueManager.callFunc("push", itemNode) + end sub + + ' A music album. + ' Play the entire album starting with track 1. + sub album(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + + ' grab list of songs in the album + albumSongs = api.users.GetItemsByQuery(m.global.session.user.id, { + "parentId": itemNode.id, + "imageTypeLimit": 1, + "sortBy": "SortName", + "limit": 2000, + "enableUserData": false, + "EnableTotalRecordCount": false + }) + if isValid(albumSongs) and isValidAndNotEmpty(albumSongs.items) + quickplay.pushToQueue(albumSongs.items) + end if + end sub + + ' A music artist. + ' Shuffle play all songs by artist. + sub artist(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + + ' get all songs by artist + artistSongs = api.users.GetItemsByQuery(m.global.session.user.id, { + "artistIds": itemNode.id, + "includeItemTypes": "Audio", + "sortBy": "Album", + "limit": 2000, + "imageTypeLimit": 1, + "Recursive": true, + "enableUserData": false, + "EnableTotalRecordCount": false + }) + print "artistSongs=", artistSongs + + if isValid(artistSongs) and isValidAndNotEmpty(artistSongs.items) + quickplay.pushToQueue(artistSongs.items, true) + end if + end sub + + ' A boxset. + ' Play all items inside. + sub boxset(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + + data = api.items.GetByQuery({ + "userid": m.global.session.user.id, + "parentid": itemNode.id, + "limit": 2000, + "EnableTotalRecordCount": false + }) + if isValid(data) and isValidAndNotEmpty(data.Items) + quickplay.pushToQueue(data.items) + end if + end sub + + ' A TV Show Series. + ' Play the first unwatched episode. + ' If none, shuffle play the whole series. + sub series(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + + data = api.shows.GetNextUp({ + "seriesId": itemNode.id, + "recursive": true, + "SortBy": "DatePlayed", + "SortOrder": "Descending", + "ImageTypeLimit": 1, + "UserId": m.global.session.user.id, + "EnableRewatching": false, + "DisableFirstEpisode": false, + "EnableTotalRecordCount": false + }) + + if isValid(data) and isValidAndNotEmpty(data.Items) + ' there are unwatched episodes + m.global.queueManager.callFunc("push", data.Items[0]) + else + ' next up check was empty + ' check for a resumable episode + data = api.users.GetResumeItemsByQuery(m.global.session.user.id, { + "parentId": itemNode.id, + "userid": m.global.session.user.id, + "SortBy": "DatePlayed", + "recursive": true, + "SortOrder": "Descending", + "Filters": "IsResumable", + "EnableTotalRecordCount": false + }) + print "resumeitems data=", data + if isValid(data) and isValidAndNotEmpty(data.Items) + ' play the resumable episode + if isValid(data.Items[0].UserData) and isValid(data.Items[0].UserData.PlaybackPositionTicks) + data.Items[0].startingPoint = data.Items[0].userdata.PlaybackPositionTicks + end if + m.global.queueManager.callFunc("push", data.Items[0]) + else + ' shuffle all episodes + data = api.shows.GetEpisodes(itemNode.id, { + "userid": m.global.session.user.id, + "SortBy": "Random", + "limit": 2000, + "EnableTotalRecordCount": false + }) + + if isValid(data) and isValidAndNotEmpty(data.Items) + ' add all episodes found to a playlist + quickplay.pushToQueue(data.Items) + end if + end if + end if + end sub + + ' More than one TV Show Series. + ' Shuffle play all watched episodes + sub multipleSeries(itemNodes as object) + if isValidAndNotEmpty(itemNodes) + numTotal = 0 + numLimit = 2000 + for each tvshow in itemNodes + ' grab all watched episodes for each series + showData = api.shows.GetEpisodes(tvshow.id, { + "userId": m.global.session.user.id, + "SortBy": "Random", + "imageTypeLimit": 0, + "EnableTotalRecordCount": false, + "enableImages": false + }) + + if isValid(showData) and isValidAndNotEmpty(showData.items) + playedEpisodes = [] + ' add all played episodes to queue + for each episode in showData.items + if isValid(episode.userdata) and isValid(episode.userdata.Played) + if episode.userdata.Played + playedEpisodes.push(episode) + end if + end if + end for + quickplay.pushToQueue(playedEpisodes) + + ' keep track of how many items we've seen + numTotal = numTotal + showData.items.count() + if numTotal >= numLimit + ' stop grabbing more items if we hit our limit + exit for + end if + end if + end for + if m.global.queueManager.callFunc("getCount") > 1 + m.global.queueManager.callFunc("toggleShuffle") + end if + end if + end sub + + ' A TV Show Season. + ' Play the first unwatched episode. + ' If none, play the whole season starting with episode 1. + sub season(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + + unwatchedData = api.shows.GetEpisodes(itemNode.json.SeriesId, { + "seasonId": itemNode.id, + "userid": m.global.session.user.id, + "limit": 2000, + "EnableTotalRecordCount": false + }) + + if isValid(unwatchedData) and isValidAndNotEmpty(unwatchedData.Items) + ' find the first unwatched episode + firstUnwatchedEpisodeIndex = invalid + for each item in unwatchedData.Items + if isValid(item.UserData) + if isValid(item.UserData.Played) and item.UserData.Played = false + firstUnwatchedEpisodeIndex = item.IndexNumber - 1 + if isValid(item.UserData.PlaybackPositionTicks) + item.startingPoint = item.UserData.PlaybackPositionTicks + end if + exit for + end if + end if + end for + + if isValid(firstUnwatchedEpisodeIndex) + ' add the first unwatched episode and the rest of the season to a playlist + for i = firstUnwatchedEpisodeIndex to unwatchedData.Items.count() - 1 + m.global.queueManager.callFunc("push", unwatchedData.Items[i]) + end for + else + ' try to find a "continue watching" episode + continueData = api.users.GetResumeItemsByQuery(m.global.session.user.id, { + "parentId": itemNode.id, + "userid": m.global.session.user.id, + "SortBy": "DatePlayed", + "recursive": true, + "SortOrder": "Descending", + "Filters": "IsResumable", + "EnableTotalRecordCount": false + }) + + if isValid(continueData) and isValidAndNotEmpty(continueData.Items) + ' play the resumable episode + for each item in continueData.Items + if isValid(item.UserData) and isValid(item.UserData.PlaybackPositionTicks) + item.startingPoint = item.userdata.PlaybackPositionTicks + end if + m.global.queueManager.callFunc("push", item) + end for + else + ' play the whole season in order + if isValid(unwatchedData) and isValidAndNotEmpty(unwatchedData.Items) + ' add all episodes found to a playlist + pushToQueue(unwatchedData.Items) + end if + end if + end if + end if + end sub + + ' Quick Play A Person. + ' Shuffle play all videos found + sub person(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + ' get movies and videos by the person + personMovies = api.users.GetItemsByQuery(m.global.session.user.id, { + "personIds": itemNode.id, + "includeItemTypes": "Movie,Video", + "excludeItemTypes": "Season,Series", + "recursive": true, + "limit": 2000 + }) + print "personMovies=", personMovies + + if isValid(personMovies) and isValidAndNotEmpty(personMovies.Items) + ' add each item to the queue + quickplay.pushToQueue(personMovies.Items) + end if + + ' get watched episodes by the person + personEpisodes = api.users.GetItemsByQuery(m.global.session.user.id, { + "personIds": itemNode.id, + "includeItemTypes": "Episode", + "isPlayed": true, + "excludeItemTypes": "Season,Series", + "recursive": true, + "limit": 2000 + }) + print "personEpisodes=", personEpisodes + + if isValid(personEpisodes) and isValidAndNotEmpty(personEpisodes.Items) + ' add each item to the queue + quickplay.pushToQueue(personEpisodes.Items) + end if + + if m.global.queueManager.callFunc("getCount") > 1 + m.global.queueManager.callFunc("toggleShuffle") + end if + end sub + + ' Quick Play A TVChannel + sub tvChannel(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + + stopLoadingSpinner() + group = CreateVideoPlayerGroup(itemNode.id) + m.global.sceneManager.callFunc("pushScene", group) + end sub + + ' Quick Play A Live Program + sub program(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.json) or not isValid(itemNode.json.ChannelId) then return + + stopLoadingSpinner() + group = CreateVideoPlayerGroup(itemNode.json.ChannelId) + m.global.sceneManager.callFunc("pushScene", group) + end sub + + ' Quick Play A Playlist. + ' Play the first unwatched episode. + ' If none, play the whole season starting with episode 1. + sub playlist(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + ' get playlist items + myPlaylist = api.playlists.GetItems(itemNode.id, { + "userId": m.global.session.user.id, + "limit": 2000 + }) + + if isValid(myPlaylist) and isValidAndNotEmpty(myPlaylist.Items) + ' add each item to the queue + quickplay.pushToQueue(myPlaylist.Items) + if m.global.queueManager.callFunc("getCount") > 1 + m.global.queueManager.callFunc("toggleShuffle") + end if + end if + end sub + + ' Quick Play A folder. + ' Shuffle play all items found + sub folder(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + + paramArray = { + "includeItemTypes": ["Episode", "Movie", "Video"], + "videoTypes": "VideoFile", + "sortBy": "Random", + "limit": 2000, + "imageTypeLimit": 1, + "Recursive": true, + "enableUserData": false, + "EnableTotalRecordCount": false + } + ' modify api query based on folder type + folderType = Lcase(itemNode.json.type) + if folderType = "studio" + paramArray["studioIds"] = itemNode.id + else if folderType = "genre" + paramArray["genreIds"] = itemNode.id + if isValid(itemNode.json.MovieCount) and itemNode.json.MovieCount > 0 + paramArray["includeItemTypes"] = "Movie" + end if + else if folderType = "musicgenre" + paramArray["genreIds"] = itemNode.id + paramArray.delete("videoTypes") + paramArray["includeItemTypes"] = "Audio" + else + paramArray["parentId"] = itemNode.id + end if + ' look for tv series instead of video files + if isValid(itemNode.json.SeriesCount) and itemNode.json.SeriesCount > 0 + paramArray["includeItemTypes"] = "Series" + paramArray.Delete("videoTypes") + end if + ' get folder items + folderData = api.users.GetItemsByQuery(m.global.session.user.id, paramArray) + print "folderData=", folderData + + if isValid(folderData) and isValidAndNotEmpty(folderData.items) + if isValid(itemNode.json.SeriesCount) and itemNode.json.SeriesCount > 0 + if itemNode.json.SeriesCount = 1 + quickplay.series(folderData.items[0]) + else + quickplay.multipleSeries(folderData.items) + end if + else + quickplay.pushToQueue(folderData.items, true) + end if + end if + end sub + + ' Quick Play A CollectionFolder. + ' Shuffle play the items inside + ' with some differences based on collectionType. + sub collectionFolder(itemNode as object) + if not isValid(itemNode) or not isValid(itemNode.id) then return + ' play depends on the kind of files inside the collectionfolder + print "attempting to quickplay a collection folder" + collectionType = LCase(itemNode.collectionType) + print "collectionType=", collectionType + + if collectionType = "movies" + ' get randomized list of movies inside + data = api.users.GetItemsByQuery(m.global.session.user.id, { + "parentId": itemNode.id, + "sortBy": "Random", + "limit": 2000 + }) + + if isValid(data) and isValidAndNotEmpty(data.items) + movieList = [] + ' add each item to the queue + for each item in data.Items + ' only add movies we're not currently watching + if isValid(item.userdata) and isValid(item.userdata.PlaybackPositionTicks) + if item.userdata.PlaybackPositionTicks = 0 + movieList.push(item) + end if + end if + end for + quickplay.pushToQueue(movieList) + end if + else if collectionType = "music" + ' get audio files from under this collection + ' sort songs by album then artist + songsData = api.users.GetItemsByQuery(m.global.session.user.id, { + "parentId": itemNode.id, + "includeItemTypes": "Audio", + "sortBy": "Album", + "Recursive": true, + "limit": 2000, + "imageTypeLimit": 1, + "enableUserData": false, + "EnableTotalRecordCount": false + }) + print "songsData=", songsData + if isValid(songsData) and isValidAndNotEmpty(songsData.items) + quickplay.pushToQueue(songsData.Items, true) + end if + else if collectionType = "boxsets" + ' get list of all boxsets inside + boxsetData = api.users.GetItemsByQuery(m.global.session.user.id, { + "parentId": itemNode.id, + "limit": 2000, + "imageTypeLimit": 0, + "enableUserData": false, + "EnableTotalRecordCount": false, + "enableImages": false + }) + + print "boxsetData=", boxsetData + + if isValid(boxsetData) and isValidAndNotEmpty(boxsetData.items) + ' pick a random boxset + arrayIndex = Rnd(boxsetData.items.count()) - 1 + myBoxset = boxsetData.items[arrayIndex] + ' grab list of items from boxset + print "myBoxset=", myBoxset + boxsetData = api.users.GetItemsByQuery(m.global.session.user.id, { + "parentId": myBoxset.id, + "EnableTotalRecordCount": false + }) + + if isValid(boxsetData) and isValidAndNotEmpty(boxsetData.items) + ' add all boxset items to queue + quickplay.pushToQueue(boxsetData.Items) + end if + end if + else if collectionType = "tvshows" or collectionType = "collectionfolder" + ' get list of tv shows inside + tvshowsData = api.users.GetItemsByQuery(m.global.session.user.id, { + "parentId": itemNode.id, + "sortBy": "Random", + "imageTypeLimit": 0, + "enableUserData": false, + "EnableTotalRecordCount": false, + "enableImages": false + }) + + print "tvshowsData=", tvshowsData + + if isValid(tvshowsData) and isValidAndNotEmpty(tvshowsData.items) + quickplay.multipleSeries(tvshowsData.items) + end if + else if collectionType = "musicvideos" + ' get randomized list of videos inside + data = api.users.GetItemsByQuery(m.global.session.user.id, { + "parentId": itemNode.id, + "includeItemTypes": "MusicVideo", + "sortBy": "Random", + "Recursive": true, + "limit": 2000, + "imageTypeLimit": 1, + "enableUserData": false, + "EnableTotalRecordCount": false + }) + print "data=", data + if isValid(data) and isValidAndNotEmpty(data.items) + quickplay.pushToQueue(data.Items) + end if + ' else if collectionType = "homevideos" ' also used for a "Photo" library + else + print "Quick Play WARNING: Unknown collection type" + end if + end sub + + ' Quick Play A UserView. + ' Play logic depends on "collectionType". + sub userView(itemNode as object) + ' play depends on the kind of files inside the collectionfolder + collectionType = LCase(itemNode.collectionType) + print "collectionType=", collectionType + + if collectionType = "playlists" + ' get list of all playlists inside + playlistData = api.users.GetItemsByQuery(m.global.session.user.id, { + "parentId": itemNode.id, + "imageTypeLimit": 0, + "enableUserData": false, + "EnableTotalRecordCount": false, + "enableImages": false + }) + + print "playlistData=", playlistData + + if isValid(playlistData) and isValidAndNotEmpty(playlistData.items) + ' pick a random playlist + arrayIndex = Rnd(playlistData.items.count()) - 1 + myPlaylist = playlistData.items[arrayIndex] + ' grab list of items from playlist + print "myPlaylist=", myPlaylist + playlistItems = api.playlists.GetItems(myPlaylist.id, { + "userId": m.global.session.user.id, + "EnableTotalRecordCount": false, + "limit": 2000 + }) + ' validate api results + if isValid(playlistItems) and isValidAndNotEmpty(playlistItems.items) + quickplay.pushToQueue(playlistItems.items, true) + end if + end if + else if collectionType = "livetv" + ' get list of all tv channels + channelData = api.users.GetItemsByQuery(m.global.session.user.id, { + "includeItemTypes": "TVChannel", + "sortBy": "Random", + "Recursive": true, + "imageTypeLimit": 0, + "enableUserData": false, + "EnableTotalRecordCount": false, + "enableImages": false + }) + print "channelData=", channelData + + if isValid(channelData) and isValidAndNotEmpty(channelData.items) + ' pick a random channel + arrayIndex = Rnd(channelData.items.count()) - 1 + myChannel = channelData.items[arrayIndex] + print "myChannel=", myChannel + ' play channel + quickplay.tvChannel(myChannel) + end if + else + print "Quick Play CollectionFolder WARNING: Unknown collection type" + end if + end sub + +end namespace diff --git a/source/utils/session.bs b/source/utils/session.bs index 56548aa56..43d950b82 100644 --- a/source/utils/session.bs +++ b/source/utils/session.bs @@ -8,6 +8,7 @@ namespace session sub Init() m.global.addFields({ session: { + "memoryLevel": "normal", server: {}, user: { Configuration: {},