2023-11-11 13:41:20 +00:00
<!DOCTYPE html> < html lang = "en" style = "font-size:16px" > < head > < meta charset = "utf-8" > < meta name = "viewport" content = "width=device-width,initial-scale=1" > < title > Source: source/api/Items.bs< / title > <!-- [if lt IE 9]>
< script src = "//html5shiv.googlecode.com/svn/trunk/html5.js" > < / script >
2023-12-05 16:56:00 +00:00
<![endif]--> < script src = "scripts/third-party/hljs.js" defer = "defer" > < / script > < script src = "scripts/third-party/hljs-line-num.js" defer = "defer" > < / script > < script src = "scripts/third-party/popper.js" defer = "defer" > < / script > < script src = "scripts/third-party/tippy.js" defer = "defer" > < / script > < script src = "scripts/third-party/tocbot.min.js" > < / script > < script > var baseURL = "/" , locationPathname = "" ; baseURL = ( baseURL = ( baseURL = "https://jellyfin.github.io/jellyfin-roku/" ) . replace ( /https?:\/\//i , "" ) ) . substr ( baseURL . indexOf ( "/" ) ) < / script > < link rel = "stylesheet" href = "styles/clean-jsdoc-theme.min.css" > < svg aria-hidden = "true" version = "1.1" xmlns = "http://www.w3.org/2000/svg" xmlns:xlink = "http://www.w3.org/1999/xlink" style = "display:none" > < defs > < symbol id = "copy-icon" viewbox = "0 0 488.3 488.3" > < g > < path d = "M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z" / > < path d = "M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z" / > < / g > < / symbol > < symbol id = "search-icon" viewBox = "0 0 512 512" > < g > < g > < path d = "M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z" / > < / g > < / g > < g > < g > < path d = "M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z" / > < / g > < / g > < / symbol > < symbol id = "font-size-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z" / > < / symbol > < symbol id = "add-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z" / > < / symbol > < symbol id = "minus-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M5 11h14v2H5z" / > < / symbol > < symbol id = "dark-theme-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z" / > < / symbol > < symbol id = "light-theme-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z" / > < / symbol > < symbol id = "reset-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z" / > < / symbol > < symbol id = "down-icon" viewBox = "0 0 16 16" > < path fill-rule = "evenodd" clip-rule = "evenodd" d = "M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z" > < / path > < / symbol > < symbol id = "codepen-icon" viewBox = "0 0 24 24" > < path fill = "none" d =
2023-10-27 02:20:25 +00:00
function ItemGetPlaybackInfo(id as string, startTimeTicks = 0 as longinteger)
2023-10-06 03:18:36 +00:00
params = {
"UserId": m.global.session.user.id,
"StartTimeTicks": startTimeTicks,
"IsPlayback": true,
"AutoOpenLiveStream": true,
"MaxStreamingBitrate": "140000000"
}
resp = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
return getJson(resp)
end function
function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioTrackIndex = -1 as integer, subtitleTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger)
body = {
"DeviceProfile": getDeviceProfile()
}
params = {
"UserId": m.global.session.user.id,
"StartTimeTicks": startTimeTicks,
"IsPlayback": true,
"AutoOpenLiveStream": true,
"MaxStreamingBitrate": "140000000",
"MaxStaticBitrate": "140000000",
"SubtitleStreamIndex": subtitleTrackIndex
}
if mediaSourceId < > "" then params.MediaSourceId = mediaSourceId
if audioTrackIndex > -1 then params.AudioStreamIndex = audioTrackIndex
req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
req.SetRequest("POST")
return postJson(req, FormatJson(body))
end function
' Search across all libraries
function searchMedia(query as string)
if query < > ""
2023-10-29 22:25:19 +00:00
data = api.users.GetItemsByQuery(m.global.session.user.id, {
2023-10-06 03:18:36 +00:00
"searchTerm": query,
"IncludePeople": true,
"IncludeMedia": true,
"IncludeShows": true,
"IncludeGenres": true,
"IncludeStudios": true,
"IncludeArtists": true,
"IncludeItemTypes": "LiveTvChannel,Movie,BoxSet,Series,Episode,Video,Person,Audio,MusicAlbum,MusicArtist,Playlist",
"EnableTotalRecordCount": false,
"ImageTypeLimit": 1,
"Recursive": true,
"limit": 100
})
results = []
2023-10-29 22:25:19 +00:00
for each item in data.Items
2023-10-06 03:18:36 +00:00
tmp = CreateObject("roSGNode", "SearchData")
tmp.image = PosterImage(item.id)
tmp.json = item
results.push(tmp)
end for
2023-10-29 22:25:19 +00:00
data.Items = results
2023-10-06 03:18:36 +00:00
return data
end if
return []
end function
' MetaData about an item
function ItemMetaData(id as string)
url = Substitute("Users/{0}/Items/{1}", m.global.session.user.id, id)
2023-11-10 23:51:00 +00:00
resp = APIRequest(url, { "fields": "Chapters" })
2023-10-06 03:18:36 +00:00
data = getJson(resp)
if data = invalid then return invalid
imgParams = {}
if data.type < > "Audio"
if data.UserData < > invalid and data.UserData.PlayedPercentage < > invalid
param = { "PercentPlayed": data.UserData.PlayedPercentage }
imgParams.Append(param)
end if
end if
if data.type = "Movie" or data.type = "MusicVideo"
tmp = CreateObject("roSGNode", "MovieData")
tmp.image = PosterImage(data.id, imgParams)
tmp.json = data
return tmp
else if data.type = "Series"
tmp = CreateObject("roSGNode", "SeriesData")
tmp.image = PosterImage(data.id)
tmp.json = data
return tmp
else if data.type = "Episode"
' param = { "AddPlayedIndicator": data.UserData.Played }
' imgParams.Append(param)
tmp = CreateObject("roSGNode", "TVEpisodeData")
tmp.image = PosterImage(data.id, imgParams)
tmp.json = data
return tmp
else if data.type = "BoxSet" or data.type = "Playlist"
tmp = CreateObject("roSGNode", "CollectionData")
tmp.image = PosterImage(data.id, imgParams)
tmp.json = data
return tmp
else if data.type = "Season"
tmp = CreateObject("roSGNode", "TVSeasonData")
tmp.image = PosterImage(data.id)
tmp.json = data
return tmp
else if data.type = "Video"
tmp = CreateObject("roSGNode", "VideoData")
tmp.image = PosterImage(data.id)
tmp.json = data
return tmp
else if data.type = "Trailer"
tmp = CreateObject("roSGNode", "VideoData")
tmp.json = data
return tmp
else if data.type = "TvChannel" or data.type = "Program"
tmp = CreateObject("roSGNode", "ChannelData")
tmp.image = PosterImage(data.id)
tmp.isFavorite = data.UserData.isFavorite
tmp.json = data
return tmp
else if data.type = "Person"
tmp = CreateObject("roSGNode", "PersonData")
tmp.image = PosterImage(data.id, { "MaxWidth": 300, "MaxHeight": 450 })
tmp.json = data
return tmp
else if data.type = "MusicArtist"
' User clicked on an artist and wants to see the list of their albums
tmp = CreateObject("roSGNode", "MusicArtistData")
tmp.image = PosterImage(data.id)
tmp.json = data
return tmp
else if data.type = "MusicAlbum"
' User clicked on an album and wants to see the list of songs
tmp = CreateObject("roSGNode", "MusicAlbumSongListData")
tmp.image = PosterImage(data.id)
tmp.json = data
return tmp
else if data.type = "Audio"
' User clicked on a song and wants it to play
tmp = CreateObject("roSGNode", "MusicSongData")
' Try using song's parent for poster image
tmp.image = PosterImage(data.ParentId, { "MaxWidth": 500, "MaxHeight": 500 })
' Song's parent poster image is no good, try using the song's poster image
if tmp.image = invalid
tmp.image = PosterImage(data.id, { "MaxWidth": 500, "MaxHeight": 500 })
end if
tmp.json = data
return tmp
else if data.type = "Recording"
' We know it's "Recording", but we don't do any special preprocessing
' for this data type at the moment, so just return the json.
return data
else
print "Items.brs::ItemMetaData processed unhandled type: " data.type
' Return json if we don't know what it is
return data
end if
end function
' Music Artist Data
function ArtistOverview(name as string)
req = createObject("roUrlTransfer")
url = Substitute("Artists/{0}", req.escape(name))
resp = APIRequest(url)
data = getJson(resp)
if data = invalid then return invalid
return data.overview
end function
' Get list of albums belonging to an artist
function MusicAlbumList(id as string)
url = Substitute("Users/{0}/Items", m.global.session.user.id)
resp = APIRequest(url, {
"AlbumArtistIds": id,
"includeitemtypes": "MusicAlbum",
"sortBy": "SortName",
"Recursive": true
})
data = getJson(resp)
results = []
for each item in data.Items
tmp = CreateObject("roSGNode", "MusicAlbumData")
tmp.image = PosterImage(item.id)
tmp.json = item
results.push(tmp)
end for
data.Items = results
return data
end function
' Get list of albums an artist appears on
function AppearsOnList(id as string)
url = Substitute("Users/{0}/Items", m.global.session.user.id)
resp = APIRequest(url, {
"ContributingArtistIds": id,
"ExcludeItemIds": id,
"includeitemtypes": "MusicAlbum",
"sortBy": "PremiereDate,ProductionYear,SortName",
"SortOrder": "Descending",
"Recursive": true
})
data = getJson(resp)
results = []
for each item in data.Items
tmp = CreateObject("roSGNode", "MusicAlbumData")
tmp.image = PosterImage(item.id)
tmp.json = item
results.push(tmp)
end for
data.Items = results
return data
end function
' Get list of songs belonging to an artist
2023-10-28 21:26:12 +00:00
function GetSongsByArtist(id as string, params = {} as object)
2023-10-06 03:18:36 +00:00
url = Substitute("Users/{0}/Items", m.global.session.user.id)
2023-10-28 21:26:12 +00:00
paramArray = {
2023-10-06 03:18:36 +00:00
"AlbumArtistIds": id,
"includeitemtypes": "Audio",
"sortBy": "SortName",
"Recursive": true
2023-10-28 21:26:12 +00:00
}
' overwrite defaults with the params provided
for each param in params
paramArray.AddReplace(param, params[param])
end for
2023-10-06 03:18:36 +00:00
2023-10-28 21:26:12 +00:00
resp = APIRequest(url, paramArray)
2023-10-06 03:18:36 +00:00
data = getJson(resp)
results = []
if data = invalid then return invalid
if data.Items = invalid then return invalid
if data.Items.Count() = 0 then return invalid
for each item in data.Items
tmp = CreateObject("roSGNode", "MusicAlbumData")
tmp.image = PosterImage(item.id)
tmp.json = item
results.push(tmp)
end for
data.Items = results
return data
end function
' Get Items that are under the provided item
function PlaylistItemList(id as string)
url = Substitute("Playlists/{0}/Items", id)
resp = APIRequest(url, {
"UserId": m.global.session.user.id
})
results = []
data = getJson(resp)
if data = invalid then return invalid
if data.Items = invalid then return invalid
if data.Items.Count() = 0 then return invalid
for each item in data.Items
tmp = CreateObject("roSGNode", "PlaylistData")
tmp.image = PosterImage(item.id)
tmp.json = item
results.push(tmp)
end for
data.Items = results
return data
end function
' Get Songs that are on an Album
function MusicSongList(id as string)
url = Substitute("Users/{0}/Items", m.global.session.user.id, id)
resp = APIRequest(url, {
"UserId": m.global.session.user.id,
"parentId": id,
"includeitemtypes": "Audio",
"sortBy": "SortName"
})
results = []
data = getJson(resp)
if data = invalid then return invalid
if data.Items = invalid then return invalid
if data.Items.Count() = 0 then return invalid
for each item in data.Items
tmp = CreateObject("roSGNode", "MusicSongData")
tmp.image = PosterImage(item.id)
tmp.json = item
results.push(tmp)
end for
data.Items = results
return data
end function
' Get Songs that are on an Album
function AudioItem(id as string)
url = Substitute("Users/{0}/Items/{1}", m.global.session.user.id, id)
resp = APIRequest(url, {
"UserId": m.global.session.user.id,
"includeitemtypes": "Audio",
"sortBy": "SortName"
})
return getJson(resp)
end function
' Get Instant Mix based on item
function CreateInstantMix(id as string)
url = Substitute("/Items/{0}/InstantMix", id)
resp = APIRequest(url, {
"UserId": m.global.session.user.id,
"Limit": 201
})
return getJson(resp)
end function
' Get Instant Mix based on item
function CreateArtistMix(id as string)
url = Substitute("Users/{0}/Items", m.global.session.user.id)
resp = APIRequest(url, {
"ArtistIds": id,
"Recursive": "true",
"MediaTypes": "Audio",
"Filters": "IsNotFolder",
"SortBy": "SortName",
"Limit": 300,
"Fields": "Chapters",
"ExcludeLocationTypes": "Virtual",
"EnableTotalRecordCount": false,
"CollapseBoxSetItems": false
})
return getJson(resp)
end function
' Get Intro Videos for an item
function GetIntroVideos(id as string)
url = Substitute("Users/{0}/Items/{1}/Intros", m.global.session.user.id, id)
resp = APIRequest(url, {
"UserId": m.global.session.user.id
})
return getJson(resp)
end function
function AudioStream(id as string)
songData = AudioItem(id)
if songData < > invalid
content = createObject("RoSGNode", "ContentNode")
if songData.title < > invalid
content.title = songData.title
end if
playbackInfo = ItemPostPlaybackInfo(songData.id, songData.mediaSources[0].id)
if playbackInfo < > invalid
content.id = playbackInfo.PlaySessionId
if useTranscodeAudioStream(playbackInfo)
' Transcode the audio
content.url = buildURL(playbackInfo.mediaSources[0].TranscodingURL)
else
' Direct Stream the audio
params = {
"Static": "true",
"Container": songData.mediaSources[0].container,
"MediaSourceId": songData.mediaSources[0].id
}
content.streamformat = songData.mediaSources[0].container
content.url = buildURL(Substitute("Audio/{0}/stream", songData.id), params)
end if
else
return invalid
end if
return content
else
return invalid
end if
end function
function useTranscodeAudioStream(playbackInfo)
return playbackInfo.mediaSources[0] < > invalid and playbackInfo.mediaSources[0].TranscodingURL < > invalid
end function
function BackdropImage(id as string)
imgParams = { "maxHeight": "720", "maxWidth": "1280" }
return ImageURL(id, "Backdrop", imgParams)
end function
' Seasons for a TV Show
function TVSeasons(id as string) as dynamic
url = Substitute("Shows/{0}/Seasons", id)
resp = APIRequest(url, { "UserId": m.global.session.user.id })
data = getJson(resp)
' validate data
if data = invalid or data.Items = invalid then return invalid
results = []
for each item in data.Items
imgParams = { "AddPlayedIndicator": item.UserData.Played }
2023-10-28 21:26:12 +00:00
tmp = CreateObject("roSGNode", "TVSeasonData")
2023-10-06 03:18:36 +00:00
tmp.image = PosterImage(item.id, imgParams)
tmp.json = item
results.push(tmp)
end for
data.Items = results
return data
end function
2023-10-27 02:20:25 +00:00
' Returns a list of TV Shows for a given TV Show and season
' Accepts strings for the TV Show Id and the season Id
function TVEpisodes(showId as string, seasonId as string) as dynamic
' Get and validate data
data = api.shows.GetEpisodes(showId, { "seasonId": seasonId, "UserId": m.global.session.user.id, "fields": "MediaStreams,MediaSources" })
2023-10-06 03:18:36 +00:00
if data = invalid or data.Items = invalid then return invalid
results = []
for each item in data.Items
tmp = CreateObject("roSGNode", "TVEpisodeData")
2023-10-27 02:20:25 +00:00
tmp.image = PosterImage(item.id, { "maxWidth": 400, "maxheight": 250 })
if isValid(tmp.image)
2023-10-06 03:18:36 +00:00
tmp.image.posterDisplayMode = "scaleToZoom"
end if
tmp.json = item
tmpMetaData = ItemMetaData(item.id)
2023-10-27 02:20:25 +00:00
2023-10-06 03:18:36 +00:00
' validate meta data
2023-10-27 02:20:25 +00:00
if isValid(tmpMetaData) and isValid(tmpMetaData.overview)
2023-10-06 03:18:36 +00:00
tmp.overview = tmpMetaData.overview
end if
results.push(tmp)
end for
data.Items = results
return data
end function
2023-10-27 02:20:25 +00:00
' Returns a list of extra features for a TV Show season
' Accepts a string that is a TV Show season id
function TVSeasonExtras(seasonId as string) as dynamic
' Get and validate TV extra features data
data = api.users.GetSpecialFeatures(m.global.session.user.id, seasonId)
if not isValid(data) then return invalid
results = []
for each item in data
tmp = CreateObject("roSGNode", "TVEpisodeData")
tmp.image = PosterImage(item.id, { "maxWidth": 400, "maxheight": 250 })
if isValid(tmp.image)
tmp.image.posterDisplayMode = "scaleToZoom"
end if
tmp.json = item
' Force item type to Video so episode auto queue is not attempted
tmp.type = "Video"
tmpMetaData = ItemMetaData(item.id)
' Validate meta data
if isValid(tmpMetaData) and isValid(tmpMetaData.overview)
tmp.overview = tmpMetaData.overview
end if
results.push(tmp)
end for
' Build that data format that the TVEpisodeRow expects
return { Items: results }
end function
2023-10-06 03:18:36 +00:00
function TVEpisodeShuffleList(show_id as string)
url = Substitute("Shows/{0}/Episodes", show_id)
resp = APIRequest(url, {
"UserId": m.global.session.user.id,
"Limit": 200,
"sortBy": "Random"
})
data = getJson(resp)
results = []
for each item in data.Items
tmp = CreateObject("roSGNode", "TVEpisodeData")
tmp.json = item
results.push(tmp)
end for
data.Items = results
return data
end function
2023-12-05 16:56:00 +00:00
< / code > < / pre > < / article > < / section > < footer class = "footer" id = "PeOAagUepe" > < div class = "wrapper" > < span class = "jsdoc-message" > Automatically generated using < a href = "https://github.com/jsdoc/jsdoc" target = "_blank" > JSDoc< / a > and the < a href = "https://github.com/ankitskvmdam/clean-jsdoc-theme" target = "_blank" > clean-jsdoc-theme< / a > .< / span > < / div > < / footer > < / div > < / div > < / div > < div class = "search-container" id = "PkfLWpAbet" style = "display:none" > < div class = "wrapper" id = "iCxFxjkHbP" > < button class = "icon-button search-close-button" id = "VjLlGakifb" aria-label = "close search" > < svg > < use xlink:href = "#close-icon" > < / use > < / svg > < / button > < div class = "search-box-c" > < svg > < use xlink:href = "#search-icon" > < / use > < / svg > < input type = "text" id = "vpcKVYIppa" class = "search-input" placeholder = "Search..." autofocus > < / div > < div class = "search-result-c" id = "fWwVHRuDuN" > < span class = "search-result-c-text" > Type anything to view search result< / span > < / div > < / div > < / div > < div class = "mobile-menu-icon-container" > < button class = "icon-button" id = "mobile-menu" data-isopen = "false" aria-label = "menu" > < svg > < use xlink:href = "#menu-icon" > < / use > < / svg > < / button > < / div > < div id = "mobile-sidebar" class = "mobile-sidebar-container" > < div class = "mobile-sidebar-wrapper" > < a href = "/" class = "sidebar-title sidebar-title-anchor" > jellyfin-roku Code Documentation< / a > < div class = "mobile-nav-links" > < div class = "external-link navbar-item" > < a id = "jellyfin-link-mobile" href = "https://jellyfin.org/" target = "_blank" > Jellyfin< / a > < / div > < div class = "external-link navbar-item" > < a id = "github-link-mobile" href = "https://github.com/jellyfin/jellyfin-roku" target = "_blank" > GitHub< / a > < / div > < div class = "external-link navbar-item" > < a id = "forum-link-mobile" href = "https://forum.jellyfin.org/f-roku-development" target = "_blank" > Forum< / a > < / div > < div class = "external-link navbar-item" > < a id = "matrix-link-mobile" href = "https://matrix.to/#/#jellyfin-dev-roku:matrix.org" target = "_blank" > Matrix< / a > < / div > < / div > < div class = "mobile-sidebar-items-c" > < div class = "sidebar-section-title with-arrow" data-isopen = "false" id = "sidebar-modules" > < div > Modules< / div > < svg > < use xlink:href = "#down-icon" > < / use > < / svg > < / div > < div class = "sidebar-section-children-container" > < div class = "sidebar-section-children" > < a href = "module-AlbumData.html" > AlbumData< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AlbumGrid.html" > AlbumGrid< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AlbumTrackList.html" > AlbumTrackList< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AlbumView.html" > AlbumView< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-Alpha.html" > Alpha< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ArtistView.html" > ArtistView< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AudioPlayer.html" > AudioPlayer< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AudioPlayerView.html" > AudioPlayerView< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AudioTrackListItem.html" > AudioTrackListItem< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ButtonGroupHoriz.html" > ButtonGroupHoriz< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ButtonGroupVert.html" > ButtonGroupVert< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ChannelData.html" > ChannelData< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-Clock.html" > Clock< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-CollectionData.html" > CollectionData< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ConfigData.html" > ConfigData< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ConfigItem.html" > ConfigItem< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ConfigList.html" > ConfigList< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ExtrasItem.html" > ExtrasItem< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ExtrasRowList.html" > ExtrasRowList< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-FavoriteItemsTask.html" > FavoriteItemsTask< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-Folder