import "pkg:/source/api/ApiClient.bs"
import "pkg:/source/api/apiPool.bs"
import "pkg:/source/utils/misc.bs"
' QuickPlayTask: Orchestrator task for all quickplay, Play All, Instant Mix,
' Trailer, and Photo Album flows that require API calls.
'
' Input AA fields:
' action (string) - dispatch key (e.g. "album", "playAllSeries", "instantMix", etc.)
' id (string) - primary item ID
' seriesId (string) - optional, for season/episode context
' collectionType (string) - optional, for collection/userView dispatch
' folderType (string) - optional, for folder sub-dispatch
' movieCount (integer) - optional, for folder series vs movie heuristic
' seriesCount (integer) - optional, for folder series vs movie heuristic
' channelId (string) - optional, for program → channel redirect
' seasonId (string) - optional, for season Play All
'
' Output AA fields:
' action (string) - "queue", "photo", or "trailer"
' items (array) - array of raw API item AAs (or node-like AAs)
' shuffle (boolean) - whether to shuffle after queuing
' firstItemStartingPoint (LongInteger) - resume position for first item (0 = start)
sub init()
m.top.functionName = "executeQuickPlay"
end sub
sub executeQuickPlay()
input = m.top.input
if not isValid(input) or not isValidAndNotEmpty(input.action) then return
action = LCase(input.action)
items = []
shouldShuffle = false
outputAction = "queue"
firstItemStartingPoint = 0& ' LongInteger
if action = "musicalbum" or action = "album"
doAlbum(input, items)
else if action = "musicartist" or action = "artist"
doArtist(input, items)
shouldShuffle = true
else if action = "boxset"
doBoxset(input, items)
else if action = "series"
firstItemStartingPoint = doSeries(input, items)
else if action = "season"
firstItemStartingPoint = doSeason(input, items)
else if action = "person"
doPerson(input, items)
shouldShuffle = true
else if action = "playlist"
doPlaylist(input, items)
shouldShuffle = true
else if action = "folder"
result = doFolder(input)
if isValid(result)
items = result.items
shouldShuffle = result.shuffle
outputAction = result.action
end if
else if action = "collectionfolder"
result = doCollectionFolder(input)
if isValid(result)
items = result.items
shouldShuffle = result.shuffle
outputAction = result.action
end if
else if action = "userview"
result = doUserView(input)
if isValid(result)
items = result.items
shouldShuffle = result.shuffle
outputAction = result.action
end if
else if action = "photoalbum"
doPhotoAlbum(input, items)
outputAction = "photo"
else if action = "playallseries"
doPlayAllSeries(input, items)
else if action = "playallseason"
doPlayAllSeason(input, items)
else if action = "playallboxset"
doPlayAllBoxset(input, items)
else if action = "playallalbum"
doPlayAllAlbum(input, items)
else if action = "playallartist"
doPlayAllArtist(input, items)
else if action = "playallplaylist"
doPlayAllPlaylist(input, items)
else if action = "instantmix"
doInstantMix(input, items)
else if action = "loadtrailers"
doLoadTrailers(input, items)
outputAction = "trailer"
else if action = "loadphotoalbum"
doPhotoAlbum(input, items)
outputAction = "photo"
end if
m.top.output = {
action: outputAction,
items: items,
shuffle: shouldShuffle,
firstItemStartingPoint: firstItemStartingPoint
}
end sub
' --- QuickPlay actions (from quickplay.bs) ---
sub doAlbum(input as object, items as object)
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": input.id,
"imageTypeLimit": 1,
"sortBy": "SortName",
"limit": 500,
"enableUserData": false,
"EnableTotalRecordCount": false
}), "qp_album")
if isValid(data) and isValidAndNotEmpty(data.items)
items.append(data.items)
end if
end sub
sub doArtist(input as object, items as object)
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"artistIds": input.id,
"includeItemTypes": "Audio",
"sortBy": "Album",
"limit": 500,
"imageTypeLimit": 1,
"Recursive": true,
"enableUserData": false,
"EnableTotalRecordCount": false
}), "qp_artist")
if isValid(data) and isValidAndNotEmpty(data.items)
items.append(data.items)
end if
end sub
sub doBoxset(input as object, items as object)
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentid": input.id,
"limit": 500,
"EnableTotalRecordCount": false
}), "qp_boxset")
if isValid(data) and isValidAndNotEmpty(data.Items)
items.append(data.Items)
end if
end sub
function doSeries(input as object, items as object) as longinteger
firstItemStartingPoint = 0&
localUser = m.global.user
' Check for a resumable (in-progress) episode first
resumeData = fetchJson(GetApi().BuildGetResumeItemsRequest({
"parentId": input.id,
"Filters": "IsResumable",
"SortBy": "DatePlayed",
"SortOrder": "Descending",
"Limit": 1,
"recursive": true,
"ImageTypeLimit": 1,
"EnableTotalRecordCount": false
}), "qp_seriesResume")
if isValid(resumeData) and isValidAndNotEmpty(resumeData.Items)
ep = resumeData.Items[0]
items.push(ep)
if isValid(ep.UserData) and isValid(ep.UserData.PlaybackPositionTicks)
firstItemStartingPoint = ep.UserData.PlaybackPositionTicks
end if
else
' Fall back to next unstarted episode
data = fetchJson(GetApi().BuildGetNextUpRequest({
"seriesId": input.id,
"Limit": 1,
"DisableFirstEpisode": false,
"ImageTypeLimit": 1,
"EnableRewatching": localUser.settings.uiDetailsEnableRewatchingNextUp,
"EnableTotalRecordCount": false
}), "qp_seriesNextUp")
if isValid(data) and isValidAndNotEmpty(data.Items)
items.push(data.Items[0])
else
' All episodes fully watched and rewatching disabled — shuffle the whole series
allEps = fetchJson(GetApi().BuildGetEpisodesRequest(input.id, {
"SortBy": "Random",
"limit": 500,
"EnableTotalRecordCount": false
}), "qp_seriesShuffle")
if isValid(allEps) and isValidAndNotEmpty(allEps.Items)
items.append(allEps.Items)
end if
end if
end if
return firstItemStartingPoint
end function
function doSeason(input as object, items as object) as longinteger
firstItemStartingPoint = 0&
seriesId = isValidAndNotEmpty(input.seriesId) ? input.seriesId : ""
if seriesId = "" then return 0&
episodesData = fetchJson(GetApi().BuildGetEpisodesRequest(seriesId, {
"seasonId": input.id,
"limit": 500,
"EnableTotalRecordCount": false
}), "qp_seasonEps")
if isValid(episodesData) and isValidAndNotEmpty(episodesData.Items)
' find the first unwatched episode
firstUnwatchedIndex = invalid
resumePosition = 0&
for each item in episodesData.Items
if isValid(item.UserData)
if isValid(item.UserData.Played) and item.UserData.Played = false
firstUnwatchedIndex = isValid(item.IndexNumber) ? item.IndexNumber - 1 : 0
if isValid(item.UserData.PlaybackPositionTicks)
resumePosition = item.UserData.PlaybackPositionTicks
end if
exit for
end if
end if
end for
if isValid(firstUnwatchedIndex)
for i = firstUnwatchedIndex to episodesData.Items.count() - 1
items.push(episodesData.Items[i])
end for
firstItemStartingPoint = resumePosition
else
' try to find a "continue watching" episode
continueData = fetchJson(GetApi().BuildGetResumeItemsRequest({
"parentId": input.id,
"SortBy": "DatePlayed",
"recursive": true,
"SortOrder": "Descending",
"Filters": "IsResumable",
"EnableTotalRecordCount": false
}), "qp_seasonContinue")
if isValid(continueData) and isValidAndNotEmpty(continueData.Items)
for each item in continueData.Items
items.push(item)
end for
' Set starting point for the first resumable episode
firstEp = continueData.Items[0]
if isValid(firstEp.UserData) and isValid(firstEp.UserData.PlaybackPositionTicks)
firstItemStartingPoint = firstEp.UserData.PlaybackPositionTicks
end if
else
' play the whole season in order
items.append(episodesData.Items)
end if
end if
end if
return firstItemStartingPoint
end function
sub doPerson(input as object, items as object)
' get movies by the person
personMovies = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"personIds": input.id,
"includeItemTypes": "Movie",
"excludeItemTypes": "Season,Series",
"recursive": true,
"limit": 500
}), "qp_personMovies")
if isValid(personMovies) and isValidAndNotEmpty(personMovies.Items)
items.append(personMovies.Items)
end if
' get watched episodes by the person
personEpisodes = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"personIds": input.id,
"includeItemTypes": "Episode,Recording",
"isPlayed": true,
"excludeItemTypes": "Season,Series",
"recursive": true,
"limit": 500
}), "qp_personEpisodes")
if isValid(personEpisodes) and isValidAndNotEmpty(personEpisodes.Items)
items.append(personEpisodes.Items)
end if
end sub
sub doPlaylist(input as object, items as object)
data = fetchJson(GetApi().BuildGetPlaylistItemsRequest(input.id, {
"limit": 500
}), "qp_playlist")
if isValid(data) and isValidAndNotEmpty(data.Items)
items.append(data.Items)
end if
end sub
sub doPhotoAlbum(input as object, items as object)
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": input.id,
"includeItemTypes": "Photo",
"sortBy": "Random",
"Recursive": true
}), "qp_photoAlbum")
if isValid(data) and isValidAndNotEmpty(data.items)
items.append(data.items)
end if
end sub
' --- Folder / CollectionFolder / UserView (complex multi-call) ---
function doFolder(input as object) as dynamic
items = []
shouldShuffle = true
outputAction = "queue"
paramArray = {
"includeItemTypes": "Episode,Recording,Movie,Video",
"videoTypes": "VideoFile",
"sortBy": "Random",
"limit": 500,
"imageTypeLimit": 1,
"Recursive": true,
"enableUserData": false,
"EnableTotalRecordCount": false
}
folderType = LCase(isValidAndNotEmpty(input.folderType) ? input.folderType : "")
seriesCount = isValid(input.seriesCount) ? input.seriesCount : 0
if folderType = "studio"
paramArray["studioIds"] = input.id
else if folderType = "genre"
paramArray["genreIds"] = input.id
if isValid(input.movieCount) and input.movieCount > 0
paramArray["includeItemTypes"] = "Movie"
end if
else if folderType = "musicgenre"
paramArray["genreIds"] = input.id
paramArray.delete("videoTypes")
paramArray["includeItemTypes"] = "Audio"
else if folderType = "photoalbum"
paramArray["parentId"] = input.id
paramArray["includeItemTypes"] = "Photo"
paramArray.delete("videoTypes")
paramArray.delete("Recursive")
else
paramArray["parentId"] = input.id
end if
if seriesCount > 0
paramArray["includeItemTypes"] = "Series"
paramArray.Delete("videoTypes")
end if
folderData = fetchJson(GetApi().BuildGetItemsByQueryRequest(paramArray), "qp_folder")
if isValid(folderData) and isValidAndNotEmpty(folderData.items)
if seriesCount > 0
if seriesCount = 1
' Single series — delegate to series quickplay
doSeries({ id: folderData.items[0].Id }, items)
shouldShuffle = false
else
doMultipleSeries(folderData.items, items)
end if
else
if folderType = "photoalbum"
outputAction = "photo"
end if
items.append(folderData.items)
end if
end if
return { action: outputAction, items: items, shuffle: shouldShuffle }
end function
function doCollectionFolder(input as object) as dynamic
items = []
shouldShuffle = true
outputAction = "queue"
collectionType = LCase(isValidAndNotEmpty(input.collectionType) ? input.collectionType : "")
if collectionType = "movies"
doVideoContainer(input, items)
shouldShuffle = false
else if collectionType = "music"
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": input.id,
"includeItemTypes": "Audio",
"sortBy": "Album",
"Recursive": true,
"limit": 500,
"imageTypeLimit": 1,
"enableUserData": false,
"EnableTotalRecordCount": false
}), "qp_collectionMusic")
if isValid(data) and isValidAndNotEmpty(data.items)
items.append(data.Items)
end if
else if collectionType = "boxsets"
' pick a random boxset and play its contents
boxsetData = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": input.id,
"limit": 500,
"imageTypeLimit": 0,
"enableUserData": false,
"EnableTotalRecordCount": false,
"enableImages": false
}), "qp_collectionBoxsets")
if isValid(boxsetData) and isValidAndNotEmpty(boxsetData.items)
arrayIndex = Rnd(boxsetData.items.count()) - 1
myBoxset = boxsetData.items[arrayIndex]
innerData = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": myBoxset.id,
"EnableTotalRecordCount": false
}), "qp_collectionBoxsetInner")
if isValid(innerData) and isValidAndNotEmpty(innerData.items)
items.append(innerData.Items)
end if
end if
shouldShuffle = false
else if collectionType = "tvshows" or collectionType = "collectionfolder"
doVideoContainer(input, items)
shouldShuffle = false
else if collectionType = "musicvideos"
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": input.id,
"includeItemTypes": "MusicVideo",
"sortBy": "Random",
"Recursive": true,
"limit": 500,
"imageTypeLimit": 1,
"enableUserData": false,
"EnableTotalRecordCount": false
}), "qp_collectionMusicVideos")
if isValid(data) and isValidAndNotEmpty(data.items)
items.append(data.Items)
end if
shouldShuffle = false
else if collectionType = "homevideos"
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": input.id,
"includeItemTypes": "Photo",
"sortBy": "Random",
"Recursive": true
}), "qp_collectionPhotos")
if isValid(data) and isValidAndNotEmpty(data.items)
items.append(data.items)
outputAction = "photo"
end if
end if
return { action: outputAction, items: items, shuffle: shouldShuffle }
end function
function doUserView(input as object) as dynamic
items = []
shouldShuffle = true
outputAction = "queue"
collectionType = LCase(isValidAndNotEmpty(input.collectionType) ? input.collectionType : "")
if collectionType = "playlists"
' pick a random playlist and play its contents
playlistData = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": input.id,
"imageTypeLimit": 0,
"enableUserData": false,
"EnableTotalRecordCount": false,
"enableImages": false
}), "qp_userViewPlaylists")
if isValid(playlistData) and isValidAndNotEmpty(playlistData.items)
arrayIndex = Rnd(playlistData.items.count()) - 1
myPlaylist = playlistData.items[arrayIndex]
playlistItems = fetchJson(GetApi().BuildGetPlaylistItemsRequest(myPlaylist.id, {
"EnableTotalRecordCount": false,
"limit": 500
}), "qp_userViewPlaylistItems")
if isValid(playlistItems) and isValidAndNotEmpty(playlistItems.items)
items.append(playlistItems.items)
end if
end if
else if collectionType = "livetv"
' pick a random channel
channelData = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"includeItemTypes": "TVChannel",
"sortBy": "Random",
"Recursive": true,
"imageTypeLimit": 0,
"enableUserData": false,
"EnableTotalRecordCount": false,
"enableImages": false
}), "qp_userViewChannels")
if isValid(channelData) and isValidAndNotEmpty(channelData.items)
arrayIndex = Rnd(channelData.items.count()) - 1
myChannel = channelData.items[arrayIndex]
' Return channel as a queue item with TvChannel type
myChannel.Type = "TvChannel"
items.push(myChannel)
end if
shouldShuffle = false
else if collectionType = "movies" or collectionType = "tvshows"
doVideoContainer(input, items)
shouldShuffle = false
end if
return { action: outputAction, items: items, shuffle: shouldShuffle }
end function
' Helper: video container logic (movies or tvshows collection)
sub doVideoContainer(input as object, items as object)
collectionType = LCase(isValidAndNotEmpty(input.collectionType) ? input.collectionType : "")
if collectionType = "movies"
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": input.id,
"sortBy": "Random",
"recursive": true,
"includeItemTypes": "Movie,Video",
"limit": 500
}), "qp_videoContainerMovies")
if isValid(data) and isValidAndNotEmpty(data.items)
for each item in data.Items
' only add videos we're not currently watching
if isValid(item.userdata) and isValid(item.userdata.PlaybackPositionTicks)
if item.userdata.PlaybackPositionTicks = 0
items.push(item)
end if
else
items.push(item)
end if
end for
end if
else if collectionType = "tvshows" or collectionType = "collectionfolder"
tvshowsData = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": input.id,
"sortBy": "Random",
"recursive": true,
"excludeItemTypes": "Season",
"imageTypeLimit": 0,
"enableUserData": false,
"EnableTotalRecordCount": false,
"enableImages": false
}), "qp_videoContainerTvShows")
if isValid(tvshowsData) and isValidAndNotEmpty(tvshowsData.items)
if tvshowsData.items[0].Type = "Series"
doMultipleSeries(tvshowsData.items, items)
else
items.append(tvshowsData.items)
end if
end if
end if
end sub
' Helper: shuffle play watched episodes from multiple series
sub doMultipleSeries(tvshows as object, items as object)
numTotal = 0
numLimit = 500
for each tvshow in tvshows
showData = fetchJson(GetApi().BuildGetEpisodesRequest(tvshow.id, {
"SortBy": "Random",
"imageTypeLimit": 0,
"EnableTotalRecordCount": false,
"enableImages": false
}), "qp_multiSeries_" + tvshow.id)
if isValid(showData) and isValidAndNotEmpty(showData.items)
for each episode in showData.items
if isValid(episode.userdata) and isValid(episode.userdata.Played)
if episode.userdata.Played
items.push(episode)
end if
end if
end for
numTotal = numTotal + showData.items.count()
if numTotal >= numLimit then exit for
end if
end for
end sub
' --- Play All actions (from Main.bs button handlers) ---
sub doPlayAllSeries(input as object, items as object)
allEpData = fetchJson(GetApi().BuildGetEpisodesRequest(input.id, {
SortBy: "IndexNumber,SortName",
SortOrder: "Ascending"
}), "qp_playAllSeries")
if isValid(allEpData) and isValid(allEpData.Items)
' Exclude Season 0 (Specials) from Play All
for each ep in allEpData.Items
if isValid(ep.ParentIndexNumber) and ep.ParentIndexNumber > 0
items.push(ep)
end if
end for
end if
end sub
sub doPlayAllSeason(input as object, items as object)
allEpData = fetchJson(GetApi().BuildGetEpisodesRequest(input.seriesId, {
SeasonId: input.id,
SortBy: "IndexNumber,SortName",
SortOrder: "Ascending"
}), "qp_playAllSeason")
if isValid(allEpData) and isValid(allEpData.Items)
items.append(allEpData.Items)
end if
end sub
sub doPlayAllBoxset(input as object, items as object)
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"ParentId": input.id,
"SortBy": "PremiereDate,ProductionYear,SortName",
"SortOrder": "Ascending",
"EnableTotalRecordCount": false
}), "qp_playAllBoxset")
if isValid(data) and isValid(data.Items)
items.append(data.Items)
end if
end sub
sub doPlayAllAlbum(input as object, items as object)
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"parentId": input.id,
"includeitemtypes": "Audio",
"SortBy": "ParentIndexNumber,IndexNumber,SortName",
"SortOrder": "Ascending",
"EnableTotalRecordCount": false
}), "qp_playAllAlbum")
if isValid(data) and isValid(data.Items)
items.append(data.Items)
end if
end sub
sub doPlayAllArtist(input as object, items as object)
data = fetchJson(GetApi().BuildGetItemsByQueryRequest({
"ArtistIds": input.id,
"Recursive": "true",
"MediaTypes": "Audio",
"Filters": "IsNotFolder",
"SortBy": "SortName",
"Limit": 300,
"Fields": "Chapters",
"ExcludeLocationTypes": "Virtual",
"EnableTotalRecordCount": false,
"CollapseBoxSetItems": false
}), "qp_playAllArtist")
if isValid(data) and isValid(data.Items)
items.append(data.Items)
end if
end sub
sub doPlayAllPlaylist(input as object, items as object)
data = fetchJson(GetApi().BuildGetPlaylistItemsRequest(input.id, {
"SortBy": "ListItemOrder",
"SortOrder": "Ascending",
"EnableTotalRecordCount": false,
"limit": 500
}), "qp_playAllPlaylist")
if isValid(data) and isValid(data.Items)
items.append(data.Items)
end if
end sub
sub doInstantMix(input as object, items as object)
data = fetchJson(GetApi().BuildGetInstantMixRequest(input.id, { "Limit": 201 }), "qp_instantMix")
if isValid(data) and isValid(data.Items)
items.append(data.Items)
end if
end sub
sub doLoadTrailers(input as object, items as object)
data = fetchJson(GetApi().BuildGetLocalTrailersRequest(input.id), "qp_trailers")
if isValid(data)
items.append(data)
end if
end sub