Merge remote-tracking branch 'upstream/unstable' into update-build-workflow

This commit is contained in:
Charles Ewert 2023-03-15 21:31:13 -04:00
commit 7366b48a2a
53 changed files with 4963 additions and 3491 deletions

View File

@ -7,33 +7,34 @@ assignees: ''
---
**Software Versions**
Jellyfin Server Version:
Roku Client Version:
## Software Versions
**Describe the bug**
- Jellyfin Server Version:
- Roku Client Version:
## Describe the bug
<!-- A clear and concise description of what the bug is. -->
**How To Reproduce**
## How To Reproduce
<!-- Steps to reproduce the behavior: -->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. Bug occurs
**Expected behavior**
## Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
**Logs**
## Logs
<!-- Please paste any log errors. -->
**Screenshots**
## Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->
**Connection Information**
Is server local or remote?
## Connection Information
Is server connection http or https?
- Is server local or remote?
- Is server connection HTTP or HTTPS?
**Additional context**
## Additional context
<!-- Add any other context about the problem here. -->

View File

@ -1,20 +1,20 @@
---
name: Enhancement request
about: Suggest an modification to an existing feature
about: Suggest a modification to an existing feature
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
## Is your feature request related to a problem? Please describe
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
## Describe the solution you'd like
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
## Describe alternatives you've considered
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
## Additional context
<!-- Add any other context or screenshots about the feature request here. -->

View File

@ -7,8 +7,8 @@ assignees: ''
---
**Describe the feature you'd like**
## Describe the feature you'd like
<!-- A clear and concise description of what you want to happen. -->
**Additional context**
## Additional context
<!-- Add any other context or screenshots about the feature request here. -->

View File

@ -2,10 +2,10 @@
Ensure your title is short, descriptive, and in the imperative mood (Fix X, Change Y, instead of Fixed X, Changed Y).
For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://jellyfin.readthedocs.io/en/latest/developer-docs/contributing/ page.
-->
**Changes**
<!-- markdownlint-disable MD041 first-line-heading -->
## Changes
<!-- Describe your changes here in 1-5 sentences. -->
**Issues**
## Issues
<!-- Tag any issues that this PR solves here.
ex. Fixes # -->

View File

@ -12,7 +12,7 @@ jobs:
dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: "lts/*"

View File

@ -26,9 +26,14 @@ jobs:
with:
packages: libxml2-utils xmlstarlet
- name: Validate XML syntax
run: xmllint --noout ./locale/*/*.ts
- name: Check XML for duplicate entries
run: xmlstarlet sel -t -m '/TS/context/message/source' -o 'Duplicate entry found in:' -f -o ' ' -c '.' -nl ./locale/*/*.ts | sort | uniq -d
run: xmllint --noout ./locale/en_US/translations.ts
- name: Save output of duplicate check
run: echo "tsDuplicates=$(xmlstarlet sel -t -m '/TS/context/message/source' -c '.' -nl ./locale/en_US/translations.ts | sort | uniq -d | awk '{ printf "%s", $0 }')" >> $GITHUB_ENV
- name: Check for duplicates
run: xmlstarlet sel -t -m '/TS/context/message/source' -f -o ' ' -c '.' -nl ./locale/en_US/translations.ts | sort | uniq -d
- name: Duplicates found
if: env.tsDuplicates != ''
run: exit 1
json:
runs-on: ubuntu-latest
steps:
@ -59,4 +64,19 @@ jobs:
run: npx ropm install
- uses: xt0rted/markdownlint-problem-matcher@98d94724052d20ca2e06c091f202e4c66c3c59fb # v2
- name: Lint markdown files
run: npm run lint-markdown
run: npm run lint-markdown
spelling:
runs-on: ubuntu-latest
steps:
- name: Clone github repo
uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: "lts/*"
cache: "npm"
- name: Install npm dependencies
run: npm ci
- name: Install roku package dependencies
run: npx ropm install
- name: Check markdown files for spelling errors
run: npm run lint-spelling

View File

@ -1,5 +0,0 @@
{
"ignore": [
"node_modules/**/*"
]
}

View File

@ -1,17 +0,0 @@
jellyfin
brightscript
vscode
roku
github
pre-release
sideload
dev
repo
hardcode
hardcoding
breakpoint
DEVGUIDE
runtime
translations.ts
en_US
ing

View File

@ -60,16 +60,16 @@ We recommend using Visual Studio Code when working on this project. The [BrightS
### Usage
1. Open the `jellyfin-roku` folder in vscode
2. Press `F5` on your keyboard or click `Run` -> `Start Debugging` from the vscode menu. ![image](https://user-images.githubusercontent.com/2544493/170696233-8ba49bf4-bebb-4655-88f3-ac45150dda02.png)
1. Open the `jellyfin-roku` folder in VSCode
2. Press `F5` on your keyboard or click `Run` -> `Start Debugging` from the VSCode menu. ![image](https://user-images.githubusercontent.com/2544493/170696233-8ba49bf4-bebb-4655-88f3-ac45150dda02.png)
3. Enter your Roku IP address and developer password when prompted
That's it! vscode will auto-package the project, sideload it to the specified device, and the channel is up and running. (assuming you remembered to put your device in [developer mode](#developer-mode))
That's it! VSCode will auto-package the project, sideload it to the specified device, and the channel is up and running. (assuming you remembered to put your device in [developer mode](#developer-mode))
### Hardcoding Roku Information
Out of the box, the Brightscript extension will prompt you to pick a Roku device (from devices found on your local network) and enter a password on every launch. If you'd prefer to hardcode this information rather than entering it every time, you can set these values in your vscode user settings:
Out of the box, the BrightScript extension will prompt you to pick a Roku device (from devices found on your local network) and enter a password on every launch. If you'd prefer to hardcode this information rather than entering it every time, you can set these values in your VSCode user settings:
```js
{
@ -95,7 +95,7 @@ Build the package
make dev
```
This will create a zip in `out/apps/Jellyfin_Roku-dev.zip`. Login to your roku's device in your browser and upload the zip file then run install.
This will create a zip in `out/apps/Jellyfin_Roku-dev.zip`. Login to your Roku's device in your browser and upload the zip file then run install.
## Method 3: Direct load to Roku Device
@ -154,7 +154,7 @@ make install
Modify code -> `make install` -> Use Roku remote to test changes -> `telnet ${ROKU_DEV_TARGET} 8085` -> `CTRL + ]` -> `quit + ENTER`
Unfortunately there is no debugger. You will need to use telnet to see log statements, warnings, and error reports. You won't always need to telnet into your device but the workflow above is typical when you are new to Brightscript or are working on tricky code.
Unfortunately there is no debugger. You will need to use telnet to see log statements, warnings, and error reports. You won't always need to telnet into your device but the workflow above is typical when you are new to BrightScript or are working on tricky code.
Install necessary packages:

View File

@ -30,7 +30,7 @@ The channel is available on the [Roku Channel Store](https://my.roku.com/add/jel
## Getting Involved<a name="get_involved"></a>
No matter what your interests or skill are, you can help to make this client better for everyone by simply using the client and letting us know if you find a problem with it. Either give us a shout on [matrix](https://matrix.to/#/+jellyfin:matrix.org) or create a github issue.
No matter what your interests or skill are, you can help to make this client better for everyone by simply using the client and letting us know if you find a problem with it. Either give us a shout on [matrix](https://matrix.to/#/+jellyfin:matrix.org) or create a GitHub issue.
Feature requests are always welcome too, but please have a read though the existing issues to see if someone has already raised one for something similar.

View File

@ -22,7 +22,7 @@ sub init()
'Parent is MarkupGrid and it's parent is the ItemGrid
m.topParent = m.top.GetParent().GetParent()
'Get the imageDisplayMode for these grid items
if m.topParent.imageDisplayMode <> invalid
if isValid(m.topParent.imageDisplayMode)
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
end if
@ -44,7 +44,7 @@ sub itemContentChanged()
m.itemText.text = itemData.Title
else if itemData.type = "Series"
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
if itemData?.json?.UserData?.UnplayedItemCount <> invalid
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
if itemData.json.UserData.UnplayedItemCount > 0
m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
@ -98,7 +98,7 @@ sub itemContentChanged()
m.posterText.height = 200
m.posterText.width = 280
else if itemData.json.type = "MusicAlbum"
else if isValid(itemData.json.type) and itemData.json.type = "MusicAlbum"
m.itemPoster.uri = itemData.PosterUrl
m.itemText.text = itemData.Title

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<component name="GridItem" extends="Group">
<children>
<maskGroup id="posterMask" maskUri="pkg:/images/postermask.png" scaleRotateCenter="[145, 212.5]" scale="[0.85,0.85]">
@ -20,4 +20,5 @@
</interface>
<script type="text/brightscript" uri="GridItem.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -16,6 +16,11 @@ sub init()
m.menus.push(m.top.findNode("sortMenu"))
m.menus.push(m.top.findNode("filterMenu"))
m.filterOptions = m.top.findNode("filterOptions")
m.filterMenu = m.top.findNode("filterMenu")
m.filterMenu.observeField("itemFocused", "onFilterFocusChange")
m.viewNames = []
m.sortNames = []
m.filterNames = []
@ -24,14 +29,46 @@ sub init()
m.fadeAnim = m.top.findNode("fadeAnim")
m.fadeOutAnimOpacity = m.top.findNode("outOpacity")
m.fadeInAnimOpacity = m.top.findNode("inOpacity")
m.showChecklistAnimation = m.top.findNode("showChecklistAnimation")
m.hideChecklistAnimation = m.top.findNode("hideChecklistAnimation")
m.buttons.observeField("focusedIndex", "buttonFocusChanged")
m.favoriteMenu.observeField("buttonSelected", "toggleFavorite")
end sub
sub showChecklist()
if m.filterOptions.opacity = 0
if m.showChecklistAnimation.state = "stopped"
m.showChecklistAnimation.control = "start"
end if
end if
end sub
sub hideChecklist()
if m.filterOptions.opacity = 1
if m.hideChecklistAnimation.state = "stopped"
m.hideChecklistAnimation.control = "start"
end if
end if
end sub
sub onFilterFocusChange()
if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() > 0
showChecklist()
else
hideChecklist()
end if
m.filterOptions.content = m.filterMenu.content.getChild(m.filterMenu.itemFocused)
if isValid(m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState)
m.filterOptions.checkedState = m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState
else
m.filterOptions.checkedState = []
end if
end sub
sub optionsSet()
' Views Tab
if m.top.options.views <> invalid
viewContent = CreateObject("roSGNode", "ContentNode")
@ -90,8 +127,19 @@ sub optionsSet()
m.selectedFilterIndex = 0
for each filterItem in m.top.options.filter
entry = filterContent.CreateChild("ContentNode")
entry = filterContent.CreateChild("OptionNode")
entry.title = filterItem.Title
entry.name = filterItem.Name
entry.delimiter = filterItem.Delimiter
if isValid(filterItem.options)
for each filterItemOption in filterItem.options
entryOption = entry.CreateChild("ContentNode")
entryOption.title = toString(filterItemOption)
end for
entry.checkedState = filterItem.checkedState
end if
m.filterNames.push(filterItem.Name)
if filterItem.selected <> invalid and filterItem.selected = true
m.selectedFilterIndex = index
@ -193,12 +241,31 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
return true
else if key = "right"
if m.menus[m.selectedItem].isInFocusChain()
' Handle Filter screen
if m.selectedItem = 2
' Selected filter has options, move cursor to it
if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() > 0
m.menus[m.selectedItem].setFocus(false)
m.filterOptions.setFocus(true)
return true
end if
end if
end if
else if key = "left"
if m.favoriteMenu.hasFocus()
m.favoriteMenu.setFocus(false)
m.menus[m.selectedItem].visible = true
m.buttons.setFocus(true)
end if
' User wants to escape filter options
if m.filterOptions.isInFocusChain()
m.filterOptions.setFocus(false)
m.menus[m.selectedItem].setFocus(true)
return true
end if
else if key = "OK"
if m.menus[m.selectedItem].isInFocusChain()
' Handle View Screen
@ -229,14 +296,58 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
end if
end if
' Handle Filter screen
if m.selectedItem = 2
m.selectedFilterIndex = m.menus[2].itemSelected
m.top.filter = m.filterNames[m.selectedFilterIndex]
' If filter has no options, select it
if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() = 0
m.menus[2].checkedItem = m.menus[2].itemSelected
m.selectedFilterIndex = m.menus[2].itemSelected
m.top.filter = m.filterNames[m.selectedFilterIndex]
m.top.filterOptions = {}
return true
end if
' Selected filter has options, move cursor to it
m.filterOptions.setFocus(true)
m.menus[m.selectedItem].setFocus(false)
return true
end if
end if
' User pressed OK from inside the filter's options
if m.filterOptions.isInFocusChain()
selectedOptions = []
for i = 0 to m.filterOptions.checkedState.count() - 1
if m.filterOptions.checkedState[i]
selectedValue = toString(m.filterOptions.content.getChild(i).title)
selectedOptions.push(selectedValue)
end if
end for
if selectedOptions.Count() > 0
m.menus[2].checkedItem = m.menus[2].itemFocused
m.selectedFilterIndex = m.menus[2].itemFocused
m.top.filter = m.filterMenu.content.getChild(m.filterMenu.itemFocused).Name
newFilter = {}
newFilter[m.top.filter] = selectedOptions.join(m.filterMenu.content.getChild(m.filterMenu.itemFocused).delimiter)
m.top.filterOptions = newFilter
else
m.menus[2].checkedItem = 0
m.selectedFilterIndex = 0
m.top.filter = m.filterNames[0]
m.top.filterOptions = {}
end if
m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState = m.filterOptions.checkedState
return true
end if
return true
else if key = "back" or key = "up"
if key = "back" then hideChecklist()
m.menus[2].visible = true ' Show Filter contents in case hidden by favorite button
if m.menus[m.selectedItem].isInFocusChain()
m.buttons.setFocus(true)
@ -244,6 +355,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true
end if
else if key = "options"
hideChecklist()
m.menus[2].visible = true ' Show Filter contents in case hidden by favorite button
m.menus[m.selectedItem].drawFocusFeedback = false
return false

View File

@ -6,15 +6,18 @@
<Poster width="1720" height="880" uri="pkg:/images/dialog.9.png" />
<LayoutGroup horizAlignment="center" translation="[860,50]" itemSpacings="[50]">
<JFButtons id="buttons" />
</LayoutGroup>
<LayoutGroup id="menuOptions" horizAlignment="center" translation="[860,200]" itemSpacings="[50]">
<Group>
<RadiobuttonList id="viewMenu" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
</RadiobuttonList>
<RadiobuttonList id="sortMenu" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="1" numRows="8" drawFocusFeedback="false">
</RadiobuttonList>
<RadiobuttonList id="filterMenu" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
<RadiobuttonList id="filterMenu" checkOnSelect="false" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
</RadiobuttonList>
</Group>
</LayoutGroup>
<CheckList opacity="0" translation="[900, 200]" id="filterOptions" numRows="8" itemSize="[250, 70]" />
<ButtonGroup translation="[1250,50]">
<Button id="favoriteMenu" iconUri="pkg:/images/icons/favorite.png" focusedIconUri="pkg:/images/icons/favorite.png" focusBitmapUri="" focusFootprintBitmapUri="" text="Favorite" showFocusFootprint="false"></Button>
</ButtonGroup>
@ -25,6 +28,16 @@
<FloatFieldInterpolator id="inOpacity" key="[0.0, 0.5, 1.0]" keyValue="[ 0, 0, 1 ]" fieldToInterp="focus.opacity" />
</Animation>
<Animation id="showChecklistAnimation" duration="0.5" repeat="false">
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0, 1]" fieldToInterp="filterOptions.opacity" />
<Vector2DFieldInterpolator key="[0.0, 1.0]" keyValue="[[860, 200], [560, 200]]" fieldToInterp="menuOptions.translation" />
</Animation>
<Animation id="hideChecklistAnimation" duration="0.5" repeat="false">
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[1, 0]" fieldToInterp="filterOptions.opacity" />
<Vector2DFieldInterpolator key="[0.0, 1.0]" keyValue="[[560, 200], [860, 200]]" fieldToInterp="menuOptions.translation" />
</Animation>
</children>
<interface>
<field id="buttons" type="nodearray" />
@ -35,8 +48,10 @@
<field id="sortField" type="string" value="SortName" />
<field id="sortAscending" type="boolean" value="false" />
<field id="filter" type="string" value="All" />
<field id="filterOptions" type="assocarray" value="" />
<field id="favorite" type="string" value="Favorite" />
</interface>
<script type="text/brightscript" uri="ItemGridOptions.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -83,6 +83,12 @@ sub loadItems()
params.append({ Filters: "IsResumable" })
end if
if isValid(m.top.filterOptions)
if m.top.filterOptions.count() > 0
params.append(m.top.filterOptions)
end if
end if
if m.top.ItemType <> ""
params.append({ IncludeItemTypes: m.top.ItemType })
end if

View File

@ -12,6 +12,7 @@
<field id="nameStartsWith" type="string" value="" />
<field id="recursive" type="boolean" value="true" />
<field id="filter" type="string" value="All" />
<field id="filterOptions" type="assocarray" value="" />
<field id="searchTerm" type="string" value="" />
<field id="studioIds" type="string" value="" />
<field id="genreIds" type="string" value="" />
@ -25,6 +26,7 @@
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
</component>

View File

@ -4,7 +4,7 @@ sub init()
m.top.limit = 60
usersettingLimit = get_user_setting("itemgrid.Limit")
if usersettingLimit <> invalid
if isValid(usersettingLimit)
m.top.limit = usersettingLimit
end if
end sub
@ -135,7 +135,7 @@ sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtit
if m.playbackInfo.MediaSources[0].MediaStreams.Count() > 0 and meta.live = false
tryDirectPlay = get_user_setting("playback.tryDirect.h264ProfileLevel") = "true" and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "h264"
tryDirectPlay = tryDirectPlay or (get_user_setting("playback.tryDirect.hevcProfileLevel") = "true" and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "hevc")
if tryDirectPlay and m.playbackInfo.MediaSources[0].TranscodingUrl <> invalid and forceTranscoding = false
if tryDirectPlay and isValid(m.playbackInfo.MediaSources[0].TranscodingUrl) and forceTranscoding = false
transcodingReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl)
if transcodingReasons.Count() = 1 and transcodingReasons[0] = "VideoLevelNotSupported"
video.directPlaySupported = true
@ -262,7 +262,7 @@ end function
function directPlaySupported(meta as object) as boolean
devinfo = CreateObject("roDeviceInfo")
if meta.json.MediaSources[0] <> invalid and meta.json.MediaSources[0].SupportsDirectPlay = false
if isValid(meta.json.MediaSources[0]) and meta.json.MediaSources[0].SupportsDirectPlay = false
return false
end if
@ -271,10 +271,10 @@ function directPlaySupported(meta as object) as boolean
end if
streamInfo = { Codec: meta.json.MediaStreams[0].codec }
if meta.json.MediaStreams[0].Profile <> invalid and meta.json.MediaStreams[0].Profile.len() > 0
if isValid(meta.json.MediaStreams[0].Profile) and meta.json.MediaStreams[0].Profile.len() > 0
streamInfo.Profile = LCase(meta.json.MediaStreams[0].Profile)
end if
if meta.json.MediaSources[0].container <> invalid and meta.json.MediaSources[0].container.len() > 0
if isValid(meta.json.MediaSources[0].container) and meta.json.MediaSources[0].container.len() > 0
'CanDecodeVideo() requires the .container to be format: “mp4”, “hls”, “mkv”, “ism”, “dash”, “ts” if its to direct stream
if meta.json.MediaSources[0].container = "mov"
streamInfo.Container = "mp4"
@ -333,12 +333,12 @@ sub autoPlayNextEpisode(videoID as string, showID as string)
resp = APIRequest(url, urlParams)
data = getJson(resp)
if data <> invalid and data.Items.Count() = 2
if isValid(data) and data.Items.Count() = 2
' setup new video node
nextVideo = invalid
' remove last videoplayer scene
m.global.sceneManager.callFunc("clearPreviousScene")
if nextVideo <> invalid
if isValid(nextVideo)
m.global.sceneManager.callFunc("pushScene", nextVideo)
else
m.global.sceneManager.callFunc("popScene")
@ -356,7 +356,7 @@ end sub
' In the future, with a custom playback info view, we can return an associated array.
function GetPlaybackInfo()
sessions = api_API().sessions.get()
if sessions <> invalid and sessions.Count() > 0
if isValid(sessions) and sessions.Count() > 0
return GetTranscodingStats(sessions[0])
end if
@ -507,7 +507,7 @@ end function
' returns the server-side track index for the appriate subtitle
function defaultSubtitleTrackFromVid(video_id) as integer
meta = ItemMetaData(video_id)
if meta?.json?.mediaSources <> invalid
if isValid(meta) and isValid(meta.json) and isValid(meta.json.mediaSources)
subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams)
default_text_subs = defaultSubtitleTrack(subtitles["all"], true) ' Find correct subtitle track (forced text)
if default_text_subs <> -1
@ -608,7 +608,7 @@ function sortSubtitles(id as string, MediaStreams)
if stream.type = "Subtitle"
url = ""
if stream.DeliveryUrl <> invalid
if isValid(stream.DeliveryUrl)
url = buildURL(stream.DeliveryUrl)
end if
@ -1199,11 +1199,15 @@ function CreateMovieDetailsGroup(movie)
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
movie = ItemMetaData(movie.id)
group.itemContent = movie
movieMetaData = ItemMetaData(movie.id)
group.itemContent = movieMetaData
group.trailerAvailable = false
trailerData = api_API().users.getlocaltrailers(get_setting("active_user"), movie.id)
activeUser = get_setting("active_user")
trailerData = invalid
if isValid(activeUser) and isValid(movie.id)
trailerData = api_API().users.getlocaltrailers(activeUser, movie.id)
end if
if isValid(trailerData)
group.trailerAvailable = trailerData.Count() > 0
end if
@ -1215,7 +1219,7 @@ function CreateMovieDetailsGroup(movie)
extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port)
extras.callFunc("loadParts", movie.json)
extras.callFunc("loadParts", movieMetaData.json)
return group
end function

View File

@ -67,10 +67,12 @@ sub init()
m.sortAscending = true
m.filter = "All"
m.filterOptions = {}
m.favorite = "Favorite"
m.loadItemsTask = createObject("roSGNode", "LoadItemsTask2")
m.loadLogoTask = createObject("roSGNode", "LoadItemsTask2")
m.getFiltersTask = createObject("roSGNode", "GetFiltersTask")
'set inital counts for overhang before content is loaded.
m.loadItemsTask.totalRecordCount = 0
@ -126,6 +128,7 @@ sub loadInitialItems()
m.sortField = get_user_setting("display." + m.top.parentItem.Id + ".sortField")
m.filter = get_user_setting("display." + m.top.parentItem.Id + ".filter")
m.filterOptions = get_user_setting("display." + m.top.parentItem.Id + ".filterOptions")
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
sortAscendingStr = get_user_setting("display." + m.top.parentItem.Id + ".sortAscending")
@ -136,8 +139,11 @@ sub loadInitialItems()
if not isValid(m.sortField) then m.sortField = "SortName"
if not isValid(m.filter) then m.filter = "All"
if not isValid(m.filterOptions) then m.filterOptions = "{}"
if not isValid(m.view) then m.view = "Movies"
m.filterOptions = ParseJson(m.filterOptions)
if sortAscendingStr = invalid or sortAscendingStr = "true"
m.sortAscending = true
else
@ -165,6 +171,7 @@ sub loadInitialItems()
m.loadItemsTask.sortField = m.sortField
m.loadItemsTask.sortAscending = m.sortAscending
m.loadItemsTask.filter = m.filter
m.loadItemsTask.filterOptions = m.filterOptions
m.loadItemsTask.startIndex = 0
' Load Item Types
@ -216,7 +223,14 @@ sub loadInitialItems()
m.loadItemsTask.observeField("content", "ItemDataLoaded")
m.spinner.visible = true
m.loadItemsTask.control = "RUN"
SetUpOptions()
m.getFiltersTask.observeField("filters", "FilterDataLoaded")
m.getFiltersTask.params = {
userid: get_setting("active_user"),
parentid: m.top.parentItem.Id,
includeitemtypes: "Movie"
}
m.getFiltersTask.control = "RUN"
end sub
' Set Movies view, sort, and filter options
@ -291,12 +305,7 @@ function inStringArray(array, searchValue) as boolean
end function
' Data to display when options button selected
sub SetUpOptions()
options = {}
options.filter = []
options.favorite = []
setMoviesOptions(options)
sub setSelectedOptions(options)
' Set selected view option
for each o in options.views
@ -316,17 +325,76 @@ sub SetUpOptions()
end if
end for
' Set selected filter option
' Set selected filter
for each o in options.filter
if o.Name = m.filter
o.Selected = true
m.options.filter = o.Name
end if
' Select selected filter options
if isValid(o.options) and isValid(m.filterOptions)
if o.options.Count() > 0 and m.filterOptions.Count() > 0
if LCase(o.Name) = LCase(m.filterOptions.keys()[0])
selectedFilterOptions = m.filterOptions[m.filterOptions.keys()[0]].split(o.delimiter)
checkedState = []
for each availableFilterOption in o.options
matchFound = false
for each selectedFilterOption in selectedFilterOptions
if LCase(toString(availableFilterOption).trim()) = LCase(selectedFilterOption.trim())
matchFound = true
end if
end for
checkedState.push(matchFound)
end for
o.checkedState = checkedState
end if
end if
end if
end for
m.options.options = options
end sub
'
' Logo Image Loaded Event Handler
sub FilterDataLoaded(msg)
options = {}
options.filter = []
options.favorite = []
setMoviesOptions(options)
data = msg.GetData()
m.getFiltersTask.unobserveField("filters")
if not isValid(data) then return
' Add Movie filters from the API data
if LCase(m.loadItemsTask.view) = "movies"
if isValid(data.genres)
options.filter.push({ "Title": tr("Genres"), "Name": "Genres", "Options": data.genres, "Delimiter": "|", "CheckedState": [] })
end if
if isValid(data.OfficialRatings)
options.filter.push({ "Title": tr("Parental Ratings"), "Name": "OfficialRatings", "Options": data.OfficialRatings, "Delimiter": "|", "CheckedState": [] })
end if
if isValid(data.Years)
options.filter.push({ "Title": tr("Years"), "Name": "Years", "Options": data.Years, "Delimiter": ",", "CheckedState": [] })
end if
end if
setSelectedOptions(options)
m.options.options = options
end sub
'
' Logo Image Loaded Event Handler
sub LogoImageLoaded(msg)
@ -384,6 +452,10 @@ sub ItemDataLoaded(msg)
m.itemGrid.setFocus(true)
m.genreList.setFocus(false)
if m.data.getChildCount() = 0
m.itemGrid.jumpToItem = 0
end if
for each item in itemData
m.data.appendChild(item)
end for
@ -709,6 +781,16 @@ sub optionsClosed()
set_user_setting("display." + m.top.parentItem.Id + ".filter", m.options.filter)
end if
if not isValid(m.options.filterOptions)
m.filterOptions = {}
end if
if not AssocArrayEqual(m.options.filterOptions, m.filterOptions)
m.filterOptions = m.options.filterOptions
reload = true
set_user_setting("display." + m.top.parentItem.Id + ".filterOptions", FormatJson(m.options.filterOptions))
end if
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
if m.options.view <> m.view
@ -720,6 +802,7 @@ sub optionsClosed()
m.loadItemsTask.NameStartsWith = " "
m.loadItemsTask.searchTerm = ""
m.filter = "All"
m.filterOptions = {}
m.sortField = "SortName"
m.sortAscending = true
@ -727,6 +810,7 @@ sub optionsClosed()
set_user_setting("display." + m.top.parentItem.Id + ".sortField", m.sortField)
set_user_setting("display." + m.top.parentItem.Id + ".sortAscending", "true")
set_user_setting("display." + m.top.parentItem.Id + ".filter", m.filter)
set_user_setting("display." + m.top.parentItem.Id + ".filterOptions", FormatJson(m.filterOptions))
reload = true
end if
@ -845,6 +929,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
m.top.alphaSelected = ""
m.loadItemsTask.filter = "All"
m.filter = "All"
m.filterOptions = {}
m.data = CreateObject("roSGNode", "ContentNode")
m.itemGrid.content = m.data
loadInitialItems()

View File

@ -63,4 +63,5 @@
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
<script type="text/brightscript" uri="MovieLibraryView.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
</component>

View File

@ -101,6 +101,7 @@ sub updateTime()
end sub
sub resetTime()
if m.hideClock then return
m.currentTimeTimer.control = "stop"
m.currentTimeTimer.control = "start"
updateTime()

View File

@ -18,9 +18,8 @@ sub init()
end sub
sub updateSize()
image = invalid
if m.top.itemContent <> invalid and m.top.itemContent.image <> invalid
if isValid(m.top.itemContent) and isValid(m.top.itemContent.image)
image = m.top.itemContent.image
end if
@ -49,7 +48,6 @@ sub updateSize()
m.backdrop.width = m.poster.width
m.backdrop.height = m.poster.height
end sub
sub itemContentChanged() as void
@ -58,7 +56,7 @@ sub itemContentChanged() as void
m.title.text = itemData.title
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
if itemData?.json?.UserData?.UnplayedItemCount <> invalid
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
if itemData.json.UserData.UnplayedItemCount > 0
m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
@ -66,12 +64,11 @@ sub itemContentChanged() as void
end if
end if
if itemData.json.lookup("Type") = "Episode" and itemData.json.IndexNumber <> invalid
if itemData.json.lookup("Type") = "Episode" and isValid(itemData.json.IndexNumber)
m.title.text = StrI(itemData.json.IndexNumber) + ". " + m.title.text
m.series.text = itemData.json.Series
m.series.visible = true
else if itemData.json.lookup("Type") = "MusicAlbum"
m.title.font = "font:SmallestSystemFont"
m.staticTitle.font = "font:SmallestSystemFont"
@ -83,8 +80,7 @@ sub itemContentChanged() as void
imageUrl = itemData.posterURL
if get_user_setting("ui.tvshows.blurunwatched") = "true"
if itemData.json.lookup("Type") = "Episode" and itemData.json.userdata <> invalid
if itemData.json.lookup("Type") = "Episode" and isValid(itemData.json.userdata)
if not itemData.json.userdata.played
imageUrl = imageUrl + "&blur=15"
end if
@ -99,25 +95,21 @@ end sub
'
' Enable title scrolling based on item Focus
sub focusChanged()
if m.top.itemHasFocus = true
m.title.repeatCount = -1
m.series.repeatCount = -1
m.staticTitle.visible = false
m.title.visible = true
' text to speech for accessibility
if m.deviceInfo.IsAudioGuideEnabled() = true
txt2Speech = CreateObject("roTextToSpeech")
txt2Speech.Flush()
txt2Speech.Say(m.title.text)
end if
else
m.title.repeatCount = 0
m.series.repeatCount = 0
m.staticTitle.visible = true
m.title.visible = false
end if
end sub

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<component name="ListPoster" extends="Group">
<children>
<Rectangle id="backdrop" />
@ -12,10 +12,11 @@
<Label id="staticTitle" horizAlign="center" font="font:SmallSystemFont" wrap="false" />
</children>
<interface>
<field id="itemContent" type="node" onChange="itemContentChanged"/>
<field id="itemContent" type="node" onChange="itemContentChanged" />
<field id="itemWidth" type="integer" />
<field id="itemHasFocus" type="boolean" onChange="focusChanged" />
</interface>
<script type="text/brightscript" uri="ListPoster.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -0,0 +1,8 @@
sub init()
m.top.functionName = "getFiltersTask"
end sub
sub getFiltersTask()
m.filters = api_API().items.getFilters(m.top.params)
m.top.filters = m.filters
end sub

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="GetFiltersTask" extends="Task">
<interface>
<field id="params" type="assocarray" />
<field id="filters" type="assocarray" />
</interface>
<script type="text/brightscript" uri="GetFiltersTask.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
</component>

View File

@ -72,7 +72,7 @@ sub onPeopleLoaded()
row = m.top.content.createChild("ContentNode")
row.Title = tr("Cast & Crew")
for each person in people
if person.json.type = "Actor" and person.json.Role <> invalid
if person.json.type = "Actor" and person.json.Role <> invalid and person.json.Role.ToStr().Trim() <> ""
person.subTitle = "as " + person.json.Role
else
person.subTitle = person.json.Type

View File

@ -33,13 +33,13 @@ sub itemContentChanged()
m.backdrop.width = itemData.imageWidth
if itemData.iconUrl <> invalid
if isValid(itemData.iconUrl)
m.itemIcon.uri = itemData.iconUrl
end if
if LCase(itemData.type) = "series"
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
if itemData?.json?.UserData?.UnplayedItemCount <> invalid
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
if itemData.json.UserData.UnplayedItemCount > 0
m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
@ -84,7 +84,7 @@ sub itemContentChanged()
end if
' Set Episode title if available
if itemData.json.EpisodeTitle <> invalid
if isValid(itemData.json.EpisodeTitle)
m.itemTextExtra.text = itemData.json.EpisodeTitle
end if
@ -106,10 +106,10 @@ sub itemContentChanged()
' Set Series and Episode Number for Extra Text
extraPrefix = ""
if itemData.json.ParentIndexNumber <> invalid
if isValid(itemData.json.ParentIndexNumber)
extraPrefix = "S" + StrI(itemData.json.ParentIndexNumber).trim()
end if
if itemData.json.IndexNumber <> invalid
if isValid(itemData.json.IndexNumber)
extraPrefix = extraPrefix + "E" + StrI(itemData.json.IndexNumber).trim()
end if
if extraPrefix.len() > 0
@ -136,10 +136,10 @@ sub itemContentChanged()
' Set Release Year and Age Rating for Extra Text
textExtra = ""
if itemData.json.ProductionYear <> invalid
if isValid(itemData.json.ProductionYear)
textExtra = StrI(itemData.json.ProductionYear).trim()
end if
if itemData.json.OfficialRating <> invalid
if isValid(itemData.json.OfficialRating)
if textExtra <> ""
textExtra = textExtra + " - " + itemData.json.OfficialRating
else
@ -181,14 +181,14 @@ sub itemContentChanged()
end if
textExtra = ""
if itemData.json.ProductionYear <> invalid
if isValid(itemData.json.ProductionYear)
textExtra = StrI(itemData.json.ProductionYear).trim()
end if
' Set Years Run for Extra Text
if itemData.json.Status = "Continuing"
textExtra = textExtra + " - Present"
else if itemData.json.Status = "Ended" and itemData.json.EndDate <> invalid
else if itemData.json.Status = "Ended" and isValid(itemData.json.EndDate)
textExtra = textExtra + " - " + LEFT(itemData.json.EndDate, 4)
end if
m.itemTextExtra.text = textExtra

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<component name="HomeItem" extends="Group">
<children>
<Rectangle id="backdrop" width="464" height="261" translation="[8,5]" />
@ -26,4 +26,5 @@
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -96,7 +96,7 @@ sub onLibrariesLoaded()
m.LoadFavoritesTask.control = "RUN"
' validate library data
if m.libraryData <> invalid and m.libraryData.count() > 0
if isValid(m.libraryData) and m.libraryData.count() > 0
userConfig = m.top.userConfig
' populate My Media row
@ -163,7 +163,7 @@ sub updateFavoritesItems()
rowIndex = getRowIndex("Favorites")
if itemData.count() < 1
if rowIndex <> invalid
if isValid(rowIndex)
' remove the row
deleteFromSizeArray(rowIndex)
homeRows.removeChildIndex(rowIndex)
@ -208,7 +208,7 @@ sub updateContinueItems()
continueRowIndex = getRowIndex("Continue Watching")
if itemData.count() < 1
if continueRowIndex <> invalid
if isValid(continueRowIndex)
' remove the row
deleteFromSizeArray(continueRowIndex)
homeRows.removeChildIndex(continueRowIndex)
@ -219,7 +219,7 @@ sub updateContinueItems()
row.title = tr("Continue Watching")
itemSize = [464, 331]
for each item in itemData
if item.json?.UserData?.PlayedPercentage <> invalid
if isValid(item.json) and isValid(item.json.UserData) and isValid(item.json.UserData.PlayedPercentage)
item.PlayedPercentage = item.json.UserData.PlayedPercentage
end if
@ -250,7 +250,7 @@ sub updateNextUpItems()
nextUpRowIndex = getRowIndex("Next Up >")
if itemData.count() < 1
if nextUpRowIndex <> invalid
if isValid(nextUpRowIndex)
' remove the row
deleteFromSizeArray(nextUpRowIndex)
homeRows.removeChildIndex(nextUpRowIndex)
@ -269,7 +269,7 @@ sub updateNextUpItems()
if nextUpRowIndex = invalid
' insert new row under "Continue Watching"
continueRowIndex = getRowIndex("Continue Watching")
if continueRowIndex <> invalid
if isValid(continueRowIndex)
updateSizeArray(itemSize, continueRowIndex + 1)
homeRows.insertChild(row, continueRowIndex + 1)
else
@ -305,7 +305,7 @@ sub updateLatestItems(msg)
if itemData.count() < 1
' remove row
if rowIndex <> invalid
if isValid(rowIndex)
deleteFromSizeArray(rowIndex)
homeRows.removeChildIndex(rowIndex)
end if
@ -355,7 +355,7 @@ sub updateOnNowItems()
onNowRowIndex = getRowIndex("On Now")
if itemData.count() < 1
if onNowRowIndex <> invalid
if isValid(onNowRowIndex)
' remove the row
deleteFromSizeArray(onNowRowIndex)
homeRows.removeChildIndex(onNowRowIndex)
@ -409,11 +409,11 @@ sub updateSizeArray(rowItemSize, rowIndex = invalid, action = "insert")
newSizeArray.Push(rowItemSize)
else if action = "insert"
newSizeArray.Push(rowItemSize)
if sizeArray[i] <> invalid
if isValid(sizeArray[i])
newSizeArray.Push(sizeArray[i])
end if
end if
else if sizeArray[i] <> invalid
else if isValid(sizeArray[i])
newSizeArray.Push(sizeArray[i])
end if
end for
@ -433,7 +433,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
if press
if key = "play"
itemToPlay = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1])
if itemToPlay <> invalid and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode")
if isValid(itemToPlay) and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode")
m.top.quickPlayNode = itemToPlay
end if
handled = true

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<component name="HomeRows" extends="RowList">
<interface>
<field id="selectedItem" type="node" alwaysNotify="true" />
@ -7,5 +7,6 @@
<function name="updateHomeRows" />
<function name="loadLibraries" />
</interface>
<script type="text/brightscript" uri="HomeRows.brs"/>
</component>
<script type="text/brightscript" uri="HomeRows.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -12,37 +12,43 @@ sub loadItems()
url = Substitute("Users/{0}/Views/", get_setting("active_user"))
resp = APIRequest(url)
data = getJson(resp)
for each item in data.Items
' Skip Books for now as we don't support it (issue #525)
if item.CollectionType <> "books"
tmp = CreateObject("roSGNode", "HomeData")
tmp.json = item
results.push(tmp)
end if
end for
if isValid(data) and isValid(data.Items)
for each item in data.Items
' Skip Books for now as we don't support it (issue #525)
if item.CollectionType <> "books"
tmp = CreateObject("roSGNode", "HomeData")
tmp.json = item
results.push(tmp)
end if
end for
end if
' Load Latest Additions to Libraries
else if m.top.itemsToLoad = "latest"
activeUser = get_setting("active_user")
if isValid(activeUser)
url = Substitute("Users/{0}/Items/Latest", activeUser)
params = {}
params["Limit"] = 16
params["ParentId"] = m.top.itemId
params["EnableImageTypes"] = "Primary,Backdrop,Thumb"
params["ImageTypeLimit"] = 1
params["EnableTotalRecordCount"] = false
url = Substitute("Users/{0}/Items/Latest", get_setting("active_user"))
params = {}
params["Limit"] = 16
params["ParentId"] = m.top.itemId
params["EnableImageTypes"] = "Primary,Backdrop,Thumb"
params["ImageTypeLimit"] = 1
params["EnableTotalRecordCount"] = false
resp = APIRequest(url, params)
data = getJson(resp)
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data
' Skip Books for now as we don't support it (issue #525)
if item.Type <> "Book"
tmp = CreateObject("roSGNode", "HomeData")
tmp.json = item
results.push(tmp)
if isValid(data)
for each item in data
' Skip Books for now as we don't support it (issue #525)
if item.Type <> "Book"
tmp = CreateObject("roSGNode", "HomeData")
tmp.json = item
results.push(tmp)
end if
end for
end if
end for
end if
' Load Next Up
else if m.top.itemsToLoad = "nextUp"
@ -74,34 +80,39 @@ sub loadItems()
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
tmp = CreateObject("roSGNode", "HomeData")
tmp.json = item
results.push(tmp)
end for
' Load Continue Watching
else if m.top.itemsToLoad = "continue"
url = Substitute("Users/{0}/Items/Resume", get_setting("active_user"))
params = {}
params["recursive"] = true
params["SortBy"] = "DatePlayed"
params["SortOrder"] = "Descending"
params["Filters"] = "IsResumable"
params["EnableTotalRecordCount"] = false
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
' Skip Books for now as we don't support it (issue #558)
if item.Type <> "Book"
if isValid(data) and isValid(data.Items)
for each item in data.Items
tmp = CreateObject("roSGNode", "HomeData")
tmp.json = item
results.push(tmp)
end for
end if
' Load Continue Watching
else if m.top.itemsToLoad = "continue"
activeUser = get_setting("active_user")
if isValid(activeUser)
url = Substitute("Users/{0}/Items/Resume", activeUser)
params = {}
params["recursive"] = true
params["SortBy"] = "DatePlayed"
params["SortOrder"] = "Descending"
params["Filters"] = "IsResumable"
params["EnableTotalRecordCount"] = false
resp = APIRequest(url, params)
data = getJson(resp)
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"
tmp = CreateObject("roSGNode", "HomeData")
tmp.json = item
results.push(tmp)
end if
end for
end if
end for
end if
else if m.top.itemsToLoad = "favorites"
@ -116,14 +127,16 @@ sub loadItems()
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
' Skip Books for now as we don't support it (issue #558)
if item.Type <> "Book"
tmp = CreateObject("roSGNode", "HomeData")
tmp.json = item
results.push(tmp)
end if
end for
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"
tmp = CreateObject("roSGNode", "HomeData")
tmp.json = item
results.push(tmp)
end if
end for
end if
else if m.top.itemsToLoad = "onNow"
url = "LiveTv/Programs/Recommended"
@ -138,12 +151,14 @@ sub loadItems()
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.Items
tmp = CreateObject("roSGNode", "HomeData")
item.ImageURL = ImageURL(item.Id)
tmp.json = item
results.push(tmp)
end for
if isValid(data) and isValid(data.Items)
for each item in data.Items
tmp = CreateObject("roSGNode", "HomeData")
item.ImageURL = ImageURL(item.Id)
tmp.json = item
results.push(tmp)
end for
end if
' Extract array of persons from Views and download full metadata for each
else if m.top.itemsToLoad = "people"
@ -195,12 +210,14 @@ sub loadItems()
url = Substitute("Items/{0}/Similar", m.top.itemId)
resp = APIRequest(url, params)
data = getJson(resp)
for each item in data.items
tmp = CreateObject("roSGNode", "ExtrasData")
tmp.posterURL = ImageUrl(item.Id, "Primary", { "Tags": item.PrimaryImageTag })
tmp.json = item
results.push(tmp)
end for
if isValid(data) and isValid(data.Items)
for each item in data.items
tmp = CreateObject("roSGNode", "ExtrasData")
tmp.posterURL = ImageUrl(item.Id, "Primary", { "Tags": item.PrimaryImageTag })
tmp.json = item
results.push(tmp)
end for
end if
else if m.top.itemsToLoad = "personMovies"
getPersonVideos("Movie", results, {})
else if m.top.itemsToLoad = "personTVShows"

View File

@ -4,7 +4,6 @@ sub init()
m.position = 0
end sub
'
' Clear all content from play queue
sub clear()
m.queue = []
@ -12,72 +11,52 @@ sub clear()
setPosition(0)
end sub
'
' Delete item from play queue at passed index
sub deleteAtIndex(index)
m.queue.Delete(index)
m.queueTypes.Delete(index)
end sub
'
' Return the number of items in the play queue
function getCount()
return m.queue.count()
end function
'
' Return the item currently in focus from the play queue
function getCurrentItem()
return getItemByIndex(m.position)
end function
'
' Return the item in the passed index from the play queue
function getItemByIndex(index)
return m.queue[index]
end function
'
' Returns current playback position within the queue
function getPosition()
return m.position
end function
'
' Move queue position back one
sub moveBack()
m.position--
end sub
'
' Move queue position ahead one
sub moveForward()
m.position++
end sub
'
' Return the current play queue
function getQueue()
return m.queue
end function
'
' Return the types of items in current play queue
function getQueueTypes()
return m.queueTypes
end function
'
' Return the unique types of items in current play queue
function getQueueUniqueTypes()
itemTypes = []
@ -91,15 +70,11 @@ function getQueueUniqueTypes()
return itemTypes
end function
'
' Return item at end of play queue without removing
function peek()
return m.queue.peek()
end function
'
' Play items in queue
sub playQueue()
nextItem = getCurrentItem()
@ -116,37 +91,28 @@ sub playQueue()
end if
end sub
'
' Remove item at end of play queue
sub pop()
m.queue.pop()
m.queueTypes.pop()
end sub
'
' Push new items to the play queue
sub push(newItem)
m.queue.push(newItem)
m.queueTypes.push(getItemType(newItem))
end sub
'
' Set the queue position
sub setPosition(newPosition)
m.position = newPosition
end sub
'
' Return the fitst item in the play queue
function top()
return getItemByIndex(0)
end function
'
' Replace play queue with passed array
sub set(items)
setPosition(0)
@ -157,10 +123,9 @@ sub set(items)
end sub
function getItemType(item) as string
if isValid(item?.json?.mediatype) and item.json.mediatype <> ""
if isValid(item) and isValid(item.json) and isValid(item.json.mediatype) and item.json.mediatype <> ""
return LCase(item.json.mediatype)
else if isValid(item?.type) and item.type <> ""
else if isValid(item) and isValid(item.type) and item.type <> ""
return LCase(item.type)
end if

View File

@ -1,7 +1,3 @@
'
' View Creators
' ----------------
' Play Audio
sub CreateAudioPlayerView()
m.view = CreateObject("roSGNode", "AudioPlayerView")
@ -26,14 +22,12 @@ sub CreateVideoPlayerView()
m.global.sceneManager.callFunc("pushScene", m.view)
end sub
'
' -----------------
' Event Handlers
' -----------------
' User requested subtitle selection popup
sub onSelectSubtitlePressed()
' None is always first in the subtitle list
subtitleData = {
data: [{ "description": "None", "type": "subtitleselection" }]
@ -80,9 +74,8 @@ end sub
' User requested playback info
sub onSelectPlaybackInfoPressed()
' Check if we already have playback info and show it in a popup
if isValid(m.playbackData?.playbackinfo)
if isValid(m.playbackData) and isValid(m.playbackData.playbackinfo)
m.global.sceneManager.callFunc("standardDialog", tr("Playback Info"), m.playbackData.playbackinfo)
return
end if
@ -95,12 +88,11 @@ sub onPlaybackInfoLoaded()
m.playbackData = m.getPlaybackInfoTask.data
' Check if we have playback info and show it in a popup
if isValid(m.playbackData?.playbackinfo)
if isValid(m.playbackData) and isValid(m.playbackData.playbackinfo)
m.global.sceneManager.callFunc("standardDialog", tr("Playback Info"), m.playbackData.playbackinfo)
end if
end sub
' Playback state change event handlers
sub onStateChange()
if LCase(m.view.state) = "finished"

View File

@ -0,0 +1,39 @@
sub init()
m.playReported = false
m.top.observeField("state", "audioStateChanged")
end sub
' State Change Event Handler
sub audioStateChanged()
currentState = LCase(m.top.state)
reportedPlaybackState = "update"
if currentState = "playing" and not m.playReported
reportedPlaybackState = "start"
m.playReported = true
else if currentState = "stopped" or currentState = "finished"
reportedPlaybackState = "stop"
m.playReported = false
end if
ReportPlayback(reportedPlaybackState)
end sub
' Report playback to server
sub ReportPlayback(state as string)
if not isValid(m.top.position) then return
params = {
"ItemId": m.global.queueManager.callFunc("getCurrentItem").id,
"PlaySessionId": m.top.content.id,
"PositionTicks": int(m.top.position) * 10000000&, 'Ensure a LongInteger is used
"IsPaused": (LCase(m.top.state) = "paused")
}
' Report playstate via global task
playstateTask = m.global.playstateTask
playstateTask.setFields({ status: state, params: params })
playstateTask.control = "RUN"
end sub

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="AudioPlayer" extends="Audio">
<script type="text/brightscript" uri="AudioPlayer.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -13,7 +13,6 @@ sub init()
m.shuffleEnabled = false
m.loopMode = ""
m.buttonCount = m.buttons.getChildCount()
m.playReported = false
m.screenSaverTimeout = 300
@ -83,10 +82,9 @@ end sub
' Creates audio node used to play song(s)
sub setupAudioNode()
m.top.audio = createObject("RoSGNode", "Audio")
m.top.audio.observeField("state", "audioStateChanged")
m.top.audio.observeField("position", "audioPositionChanged")
m.top.audio.observeField("bufferingStatus", "bufferPositionChanged")
m.global.audioPlayer.observeField("state", "audioStateChanged")
m.global.audioPlayer.observeField("position", "audioPositionChanged")
m.global.audioPlayer.observeField("bufferingStatus", "bufferPositionChanged")
end sub
' Setup playback buttons, default to Play button selected
@ -120,10 +118,10 @@ sub setupInfoNodes()
end sub
sub bufferPositionChanged()
if not isValid(m.top.audio.bufferingStatus)
if not isValid(m.global.audioPlayer.bufferingStatus)
bufferPositionBarWidth = m.seekBar.width
else
bufferPositionBarWidth = m.seekBar.width * m.top.audio.bufferingStatus.percentage
bufferPositionBarWidth = m.seekBar.width * m.global.audioPlayer.bufferingStatus.percentage
end if
' Ensure position bar is never wider than the seek bar
@ -137,16 +135,16 @@ sub bufferPositionChanged()
end sub
sub audioPositionChanged()
if m.top.audio.position = 0
if m.global.audioPlayer.position = 0
m.playPosition.width = 0
end if
if not isValid(m.top.audio.position)
if not isValid(m.global.audioPlayer.position)
playPositionBarWidth = 0
else if not isValid(m.songDuration)
playPositionBarWidth = 0
else
songPercentComplete = m.top.audio.position / m.songDuration
songPercentComplete = m.global.audioPlayer.position / m.songDuration
playPositionBarWidth = m.seekBar.width * songPercentComplete
end if
@ -202,25 +200,8 @@ end sub
sub audioStateChanged()
if m.top.audio.state = "playing"
if m.playReported
ReportPlayback()
else
ReportPlayback("start")
m.playReported = true
end if
else if m.top.audio.state = "paused"
ReportPlayback()
else if m.top.audio.state = "stopped"
ReportPlayback("stop")
m.playReported = false
else if m.top.audio.state = "finished"
ReportPlayback("stop")
m.playReported = false
end if
' Song Finished, attempt to move to next song
if m.top.audio.state = "finished"
if m.global.audioPlayer.state = "finished"
' User has enabled single song loop, play current song again
if m.loopMode = "one"
playAction()
@ -246,18 +227,18 @@ sub audioStateChanged()
end sub
function playAction() as boolean
if m.top.audio.state = "playing"
m.top.audio.control = "pause"
if m.global.audioPlayer.state = "playing"
m.global.audioPlayer.control = "pause"
' Allow screen to go to real screensaver
WriteAsciiFile("tmp:/scene.temp", "nowplaying-paused")
MoveFile("tmp:/scene.temp", "tmp:/scene")
else if m.top.audio.state = "paused"
m.top.audio.control = "resume"
else if m.global.audioPlayer.state = "paused"
m.global.audioPlayer.control = "resume"
' Write screen tracker for screensaver
WriteAsciiFile("tmp:/scene.temp", "nowplaying")
MoveFile("tmp:/scene.temp", "tmp:/scene")
else if m.top.audio.state = "finished"
m.top.audio.control = "play"
else if m.global.audioPlayer.state = "finished"
m.global.audioPlayer.control = "play"
' Write screen tracker for screensaver
WriteAsciiFile("tmp:/scene.temp", "nowplaying")
MoveFile("tmp:/scene.temp", "tmp:/scene")
@ -268,15 +249,15 @@ end function
function previousClicked() as boolean
if m.playlistTypeCount > 1 then return false
if m.global.queueManager.callFunc("getPosition") = 0 then return false
if m.top.audio.state = "playing"
m.top.audio.control = "stop"
if m.global.audioPlayer.state = "playing"
m.global.audioPlayer.control = "stop"
end if
if m.global.queueManager.callFunc("getPosition") > 0
m.global.queueManager.callFunc("moveBack")
pageContentChanged()
end if
m.global.queueManager.callFunc("moveBack")
pageContentChanged()
return true
end function
@ -363,8 +344,8 @@ function shuffleClicked() as boolean
end function
sub LoadNextSong()
if m.top.audio.state = "playing"
m.top.audio.control = "stop"
if m.global.audioPlayer.state = "playing"
m.global.audioPlayer.control = "stop"
end if
' Reset playPosition bar without animation
@ -434,9 +415,9 @@ sub onAudioStreamLoaded()
data = m.LoadAudioStreamTask.content[0]
m.LoadAudioStreamTask.unobserveField("content")
if data <> invalid and data.count() > 0
m.top.audio.content = data
m.top.audio.control = "none"
m.top.audio.control = "play"
m.global.audioPlayer.content = data
m.global.audioPlayer.control = "none"
m.global.audioPlayer.control = "play"
end if
end sub
@ -545,7 +526,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
if key = "play"
return playAction()
else if key = "back"
m.top.audio.control = "stop"
m.global.audioPlayer.control = "stop"
else if key = "rewind"
return previousClicked()
else if key = "fastforward"
@ -587,21 +568,3 @@ sub OnScreenHidden()
WriteAsciiFile("tmp:/scene.temp", "")
MoveFile("tmp:/scene.temp", "tmp:/scene")
end sub
' Report playback to server
sub ReportPlayback(state = "update" as string)
if m.top.audio.position = invalid then return
params = {
"ItemId": m.global.queueManager.callFunc("getCurrentItem").id,
"PlaySessionId": m.top.audio.content.id,
"PositionTicks": int(m.top.audio.position) * 10000000&, 'Ensure a LongInteger is used
"IsPaused": (m.top.audio.state = "paused")
}
' Report playstate via worker task
playstateTask = m.global.playstateTask
playstateTask.setFields({ status: state, params: params })
playstateTask.control = "RUN"
end sub

View File

@ -119,7 +119,6 @@
<Poster width="0" height="0" uri="pkg:/images/icons/loopIndicator1-on.png" visible="false" />
</children>
<interface>
<field id="audio" type="node" />
<field id="state" type="string" />
<field id="selectedButtonIndex" type="integer" />
</interface>

View File

@ -0,0 +1,2 @@
sub init()
end sub

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="OptionNode" extends="ContentNode">
<interface>
<field id="name" type="string" />
<field id="delimiter" type="string" />
<field id="checkedState" type="array" />
</interface>
<script type="text/brightscript" uri="OptionNode.brs" />
</component>

View File

@ -19,7 +19,7 @@ end sub
sub updateSeason()
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
if m.top.seasonData?.UserData?.UnplayedItemCount <> invalid
if isValid(m.top.seasonData) and isValid(m.top.seasonData.UserData) and isValid(m.top.seasonData.UserData.UnplayedItemCount)
if m.top.seasonData.UserData.UnplayedItemCount > 0
m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = m.top.seasonData.UserData.UnplayedItemCount
@ -57,7 +57,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true
end if
if key = "OK" or key = "play"
if m.Random.hasFocus()
randomEpisode = Rnd(m.rows.getChild(0).objects.items.count()) - 1
@ -81,7 +80,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
end if
focusedChild = m.top.focusedChild.focusedChild
if focusedChild.content = invalid then return handled
@ -94,7 +92,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
if press and key = "play" or proceed = true
m.top.lastFocus = focusedChild
itemToPlay = focusedChild.content.getChild(focusedChild.rowItemFocused[0]).getChild(0)
if itemToPlay <> invalid and itemToPlay.id <> ""
if isValid(itemToPlay) and isValid(itemToPlay.id) and itemToPlay.id <> ""
itemToPlay.type = "Episode"
m.top.quickPlayNode = itemToPlay
end if

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<component name="TVEpisodes" extends="JFGroup">
<children>
<Poster id="seasonPoster" width="300" height="450" translation="[95,175]">
@ -20,4 +20,5 @@
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -18,7 +18,7 @@ end sub
sub itemContentChanged()
item = m.top.itemContent
itemData = item.json
if itemData.indexNumber <> invalid
if isValid(itemData.indexNumber)
indexNumber = itemData.indexNumber.toStr() + ". "
else
indexNumber = ""
@ -26,7 +26,7 @@ sub itemContentChanged()
m.title.text = indexNumber + item.title
m.overview.text = item.overview
if itemData.PremiereDate <> invalid
if isValid(itemData.PremiereDate)
airDate = CreateObject("roDateTime")
airDate.FromISO8601String(itemData.PremiereDate)
m.top.findNode("aired").text = tr("Aired") + ": " + airDate.AsDateString("short-month-no-weekday")
@ -70,12 +70,12 @@ sub itemContentChanged()
end if
' Add checkmark in corner (if applicable)
if isValid(itemData?.UserData?.Played) and itemData.UserData.Played = true
if isValid(itemData.UserData) and isValid(itemData.UserData.Played) and itemData.UserData.Played = true
m.playedIndicator.visible = true
end if
' Add progress bar on bottom (if applicable)
if isValid(itemData?.UserData?.PlayedPercentage) and itemData?.UserData?.PlayedPercentage > 0
if isValid(itemData.UserData) and isValid(itemData.UserData.PlayedPercentage) and itemData.UserData.PlayedPercentage > 0
m.progressBackground.width = m.poster.width
m.progressBackground.visible = true
progressWidthInPixels = int(m.progressBackground.width * itemData.UserData.PlayedPercentage / 100)
@ -86,7 +86,7 @@ sub itemContentChanged()
videoIdx = invalid
audioIdx = invalid
if itemData.MediaStreams <> invalid
if isValid(itemData.MediaStreams)
for i = 0 to itemData.MediaStreams.Count() - 1
if itemData.MediaStreams[i].Type = "Video" and videoIdx = invalid
videoIdx = i
@ -99,12 +99,12 @@ sub itemContentChanged()
end if
m.top.findNode("audio_codec").text = tr("Audio") + ": " + itemData.mediaStreams[audioIdx].DisplayTitle
end if
if videoIdx <> invalid and audioIdx <> invalid then exit for
if isValid(videoIdx) and isValid(audioIdx) then exit for
end for
end if
m.top.findNode("video_codec").visible = videoIdx <> invalid
if audioIdx <> invalid
m.top.findNode("video_codec").visible = isValid(videoIdx)
if isValid(audioIdx)
m.top.findNode("audio_codec").visible = true
DisplayAudioAvailable(itemData.mediaStreams)
else
@ -113,7 +113,6 @@ sub itemContentChanged()
end sub
sub DisplayAudioAvailable(streams)
count = 0
for i = 0 to streams.Count() - 1
if streams[i].Type = "Audio"
@ -124,7 +123,6 @@ sub DisplayAudioAvailable(streams)
if count > 1
m.top.findnode("audio_codec_count").text = "+" + stri(count - 1).trim()
end if
end sub
function getRuntime() as integer

View File

@ -17,7 +17,7 @@ sub itemContentChanged()
itemData = item.json
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
if itemData?.UserData?.UnplayedItemCount <> invalid
if isValid(itemData.UserData) and isValid(itemData.UserData.UnplayedItemCount)
if itemData.UserData.UnplayedItemCount > 0
m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = itemData.UserData.UnplayedItemCount
@ -31,21 +31,21 @@ sub itemContentChanged()
m.top.overhangTitle = itemData.name
'Check production year, if invalid remove label
if itemData.productionYear <> invalid
if isValid(itemData.productionYear)
setFieldText("releaseYear", itemData.productionYear)
else
m.top.findNode("main_group").removeChild(m.top.findNode("releaseYear"))
end if
'Check officialRating, if invalid remove label
if itemData.officialRating <> invalid
if isValid(itemData.officialRating)
setFieldText("officialRating", itemData.officialRating)
else
m.top.findNode("main_group").removeChild(m.top.findNode("officialRating"))
end if
'Check communityRating, if invalid remove label
if itemData.communityRating <> invalid
if isValid(itemData.communityRating)
m.top.findNode("star").visible = true
setFieldText("communityRating", int(itemData.communityRating * 10) / 10)
else
@ -134,7 +134,7 @@ function getHistory() as string
airdays = itemData.airdays
airtime = itemData.airtime
if airtime <> invalid and airdays.count() = 1
if isValid(airtime) and airdays.count() = 1
airwords = airdays[0] + " at " + airtime
end if
@ -148,10 +148,10 @@ function getHistory() as string
end if
words = verb
if airwords <> invalid
if isValid(airwords)
words = words + " " + airwords
end if
if studio <> invalid
if isValid(studio)
words = words + " on " + studio
end if

17
dictionary.txt Normal file
View File

@ -0,0 +1,17 @@
Jellyfin
VSCode
BrightScript
sideload
Sideload
DEVGUIDE
ing
hardcode
Hardcoding
pre-release
breakpoint
repo
Repo
dev
Dev
assignees
HTTPS

View File

@ -6,7 +6,7 @@
<name>default</name>
<message>
<source>192.168.1.100:8096 or https://example.com/jellyfin</source>
<translation>default192.168.1.100:8096 or https://example.com/jellyfin</translation>
<translation>192.168.1.100:8096 or https://example.com/jellyfin</translation>
</message>
<message>
<source>Cancel</source>
@ -764,8 +764,8 @@
<extracomment>Name of codec used in settings menu</extracomment>
</message>
<message>
<source>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.</source>
<translation>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.</translation>
<source>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to transcoding if it fails.</source>
<translation>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to transcoding if it fails.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
@ -929,9 +929,14 @@
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</source>
<translation>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
<source>Parental Ratings</source>
<translation>Parental Ratings</translation>
<extracomment>Used in Filter menu</extracomment>
</message>
<message>
<source>Years</source>
<translation>Years</translation>
<extracomment>Used in Filter menu</extracomment>
</message>
<message>
<source>Show What's New Popup</source>
@ -965,14 +970,6 @@
<translation>Default view for Movie Libraries.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Movies (Presentation)</source>
<translation>Movies (Presentation)</translation>
</message>
<message>
<source>Movies (Grid)</source>
<translation>Movies (Grid)</translation>
</message>
<message>
<source>Item Titles</source>
<translation>Item Titles</translation>
@ -1128,4 +1125,4 @@
<extracomment>Settings Menu - Description for option</extracomment>
</message>
</context>
</TS>
</TS>

7272
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,18 @@
"@rokucommunity/bslint": "0.8.1",
"brighterscript": "0.61.3",
"ropm": "0.10.11",
"jsonlint-cli": "1.0.1",
"markdownlint-cli": "0.33.0",
"markdown-spellcheck": "1.3.1"
"jshint": "^2.13.6",
"markdownlint-cli2": "0.6.0",
"spellchecker-cli": "6.1.1"
},
"scripts": {
"postinstall": "npx ropm copy",
"validate": "npx bsc --copy-to-staging=false --create-package=false",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "bslint",
"lint-json": "jsonlint-cli **/*.json",
"lint-markdown": "markdownlint **/*.md --ignore node_modules && mdspell --en-us -r **/*.md !**/node_modules/**/*.md",
"lint-json": "jshint --extra-ext .json --verbose --exclude node_modules ./",
"lint-markdown": "markdownlint-cli2 \"**/*.md\" \"#node_modules\"",
"lint-spelling": "spellchecker -d dictionary.txt --files \"**/*.md\" \"**/.*/**/*.md\" \"!node_modules/**/*.md\"",
"check-formatting": "npx bsfmt --check",
"format": "npx bsfmt --write"
},

View File

@ -63,7 +63,7 @@
},
{
"title": "HEVC",
"description": "Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.",
"description": "Attempt Direct Play for HEVC media with unsupported profile levels before falling back to transcoding if it fails.",
"settingName": "playback.tryDirect.hevcProfileLevel",
"type": "bool",
"default": "true"

View File

@ -42,6 +42,7 @@ sub Main (args as dynamic) as void
m.global.addFields({ app_loaded: false, playstateTask: playstateTask, sceneManager: sceneManager })
m.global.addFields({ queueManager: CreateObject("roSGNode", "QueueManager") })
m.global.addFields({ audioPlayer: CreateObject("roSGNode", "AudioPlayer") })
app_start:
' First thing to do is validate the ability to use the API
@ -256,8 +257,10 @@ sub Main (args as dynamic) as void
ptr = msg.getData()
' ptr is for [row, col] of selected item... but we only have 1 row
series = msg.getRoSGNode()
node = series.seasonData.items[ptr[1]]
group = CreateSeasonDetailsGroup(series.itemContent, node)
if isValid(ptr) and ptr.count() >= 2 and isValid(ptr[1]) and isValid(series) and isValid(series.seasonData) and isValid(series.seasonData.items)
node = series.seasonData.items[ptr[1]]
group = CreateSeasonDetailsGroup(series.itemContent, node)
end if
else if isNodeEvent(msg, "musicAlbumSelected")
' If you select a Music Album from ANYWHERE, follow this flow
ptr = msg.getData()
@ -401,33 +404,33 @@ sub Main (args as dynamic) as void
' If a button is selected, we have some determining to do
btn = getButton(msg)
group = sceneManager.callFunc("getActiveScene")
if btn <> invalid and btn.id = "play-button"
if isValid(btn) and btn.id = "play-button"
' Check if a specific Audio Stream was selected
audio_stream_idx = 1
if group.selectedAudioStreamIndex <> invalid
if isValid(group) and isValid(group.selectedAudioStreamIndex)
audio_stream_idx = group.selectedAudioStreamIndex
end if
' Check to see if a specific video "version" was selected
mediaSourceId = invalid
if group.selectedVideoStreamId <> invalid
if isValid(group) and isValid(group.selectedVideoStreamId)
mediaSourceId = group.selectedVideoStreamId
end if
video_id = group.id
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx)
if video <> invalid and video.errorMsg <> "introaborted"
if isValid(video) and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
end if
if group.lastfocus.id = "main_group"
if isValid(group) and isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group"
buttons = group.findNode("buttons")
if isValid(buttons)
group.lastfocus = group.findNode("buttons")
group.lastFocus = group.findNode("buttons")
end if
end if
if group.lastFocus <> invalid
if isValid(group) and isValid(group.lastFocus)
group.lastFocus.setFocus(true)
end if
@ -440,26 +443,31 @@ sub Main (args as dynamic) as void
video_id = group.id
trailerData = api_API().users.getlocaltrailers(get_setting("active_user"), group.id)
video = invalid
video_id = trailerData[0].id
if isValid(trailerData) and isValid(trailerData[0]) and isValid(trailerData[0].id)
video_id = trailerData[0].id
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx, false, false)
end if
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx, false, false)
if video <> invalid and video.errorMsg <> "introaborted"
if isValid(video) and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
dialog.close = true
end if
if group.lastFocus <> invalid
if isValid(group) and isValid(group.lastFocus)
group.lastFocus.setFocus(true)
end if
else if btn <> invalid and btn.id = "watched-button"
movie = group.itemContent
if movie.watched
UnmarkItemWatched(movie.id)
else
MarkItemWatched(movie.id)
if isValid(movie) and isValid(movie.watched) and isValid(movie.id)
if movie.watched
UnmarkItemWatched(movie.id)
else
MarkItemWatched(movie.id)
end if
movie.watched = not movie.watched
end if
movie.watched = not movie.watched
else if btn <> invalid and btn.id = "favorite-button"
movie = group.itemContent
if movie.favorite
@ -479,11 +487,11 @@ sub Main (args as dynamic) as void
else if isNodeEvent(msg, "optionSelected")
button = msg.getRoSGNode()
group = sceneManager.callFunc("getActiveScene")
if button.id = "goto_search"
if button.id = "goto_search" and isValid(group)
' Exit out of the side panel
panel = group.findNode("options")
panel.visible = false
if group.lastFocus <> invalid
if isValid(group.lastFocus)
group.lastFocus.setFocus(true)
else
group.setFocus(true)
@ -506,7 +514,7 @@ sub Main (args as dynamic) as void
' Exit out of the side panel
panel = group.findNode("options")
panel.visible = false
if group.lastFocus <> invalid
if isValid(group) and isValid(group.lastFocus)
group.lastFocus.setFocus(true)
else
group.setFocus(true)
@ -529,41 +537,47 @@ sub Main (args as dynamic) as void
end if
else if isNodeEvent(msg, "state")
node = msg.getRoSGNode()
if m.selectedItemType = "TvChannel" and node.state = "finished"
video = CreateVideoPlayerGroup(node.id)
m.global.sceneManager.callFunc("pushScene", video)
m.global.sceneManager.callFunc("deleteSceneAtIndex", 2)
else if node.state = "finished"
node.control = "stop"
if isValid(node) and isValid(node.state)
if m.selectedItemType = "TvChannel" and node.state = "finished"
video = CreateVideoPlayerGroup(node.id)
m.global.sceneManager.callFunc("pushScene", video)
m.global.sceneManager.callFunc("deleteSceneAtIndex", 2)
else if node.state = "finished"
node.control = "stop"
' If node allows retrying using Transcode Url, give that shot
if isValid(node.retryWithTranscoding) and node.retryWithTranscoding
retryVideo = CreateVideoPlayerGroup(node.Id, invalid, node.audioIndex, true, false)
m.global.sceneManager.callFunc("popScene")
if retryVideo <> invalid
m.global.sceneManager.callFunc("pushScene", retryVideo)
end if
else if node.showID = invalid
sceneManager.callFunc("popScene")
else
if video.errorMsg = ""
autoPlayNextEpisode(node.id, node.showID)
else
' If node allows retrying using Transcode Url, give that shot
if isValid(node.retryWithTranscoding) and node.retryWithTranscoding
retryVideo = CreateVideoPlayerGroup(node.Id, invalid, node.audioIndex, true, false)
m.global.sceneManager.callFunc("popScene")
if isValid(retryVideo)
m.global.sceneManager.callFunc("pushScene", retryVideo)
end if
else if not isValid(node.showID)
sceneManager.callFunc("popScene")
else
if video.errorMsg = ""
autoPlayNextEpisode(node.id, node.showID)
else
sceneManager.callFunc("popScene")
end if
end if
end if
end if
else if type(msg) = "roDeviceInfoEvent"
event = msg.GetInfo()
group = sceneManager.callFunc("getActiveScene")
if event.exitedScreensaver = true
sceneManager.callFunc("resetTime")
if group.subtype() = "Home"
currentTime = CreateObject("roDateTime").AsSeconds()
group.timeLastRefresh = currentTime
group.callFunc("refresh")
group = sceneManager.callFunc("getActiveScene")
if isValid(group) and isValid(group.subtype())
' refresh the current view
if group.subtype() = "Home"
currentTime = CreateObject("roDateTime").AsSeconds()
group.timeLastRefresh = currentTime
group.callFunc("refresh")
end if
' todo: add other screens to be refreshed - movie detail, tv series, episode list etc.
end if
' todo: add other screens to be refreshed - movie detail, tv series, episode list etc.
else
print "Unhandled roDeviceInfoEvent:"
print msg.GetInfo()

View File

@ -337,8 +337,8 @@ function CreateMovieDetailsGroup(movie)
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
movie = ItemMetaData(movie.id)
group.itemContent = movie
movieMetaData = ItemMetaData(movie.id)
group.itemContent = movieMetaData
group.trailerAvailable = false
trailerData = api_API().users.getlocaltrailers(get_setting("active_user"), movie.id)
@ -353,7 +353,7 @@ function CreateMovieDetailsGroup(movie)
extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port)
extras.callFunc("loadParts", movie.json)
extras.callFunc("loadParts", movieMetaData.json)
stopLoadingSpinner()
return group
end function

View File

@ -31,10 +31,10 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
' Special handling for "Programs" or "Vidoes" launched from "On Now" or elsewhere on the home screen...
' basically anything that is a Live Channel.
if isValid(meta?.json?.ChannelId)
if meta.json.EpisodeTitle <> invalid
if isValid(meta.json) and isValid(meta.json.ChannelId)
if isValid(meta.json.EpisodeTitle)
meta.title = meta.json.EpisodeTitle
else if meta.json.Name <> invalid
else if isValid(meta.json.Name)
meta.title = meta.json.Name
end if
meta.showID = meta.json.id
@ -47,7 +47,7 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
end if
if m.videotype = "Episode" or m.videotype = "Series"
if isValid(meta.json.RunTimeTicks)
if isValid(meta.json) and isValid(meta.json.RunTimeTicks)
video.runTime = (meta.json.RunTimeTicks / 10000000.0)
end if
video.content.contenttype = "episode"
@ -243,11 +243,9 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
video.content.SubtitleTracks = subtitles["text"]
' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay
fully_external = false
' For h264/hevc video, Roku spec states that it supports specfic encoding levels
' The device can decode content with a Higher Encoding level but may play it back with certain
' artifacts. If the user preference is set, and the only reason the server says we need to
@ -256,7 +254,7 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
if m.playbackInfo.MediaSources[0].MediaStreams.Count() > 0 and meta.live = false
tryDirectPlay = get_user_setting("playback.tryDirect.h264ProfileLevel") = "true" and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "h264"
tryDirectPlay = tryDirectPlay or (get_user_setting("playback.tryDirect.hevcProfileLevel") = "true" and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "hevc")
if tryDirectPlay and m.playbackInfo.MediaSources[0].TranscodingUrl <> invalid and forceTranscoding = false
if tryDirectPlay and isValid(m.playbackInfo.MediaSources[0].TranscodingUrl) and forceTranscoding = false
transcodingReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl)
if transcodingReasons.Count() = 1 and transcodingReasons[0] = "VideoLevelNotSupported"
video.directPlaySupported = true
@ -324,7 +322,6 @@ end sub
function PlayIntroVideo(video_id, audio_stream_idx) as boolean
' Intro videos only play if user has cinema mode setting enabled
if get_user_setting("playback.cinemamode") = "true"
' Check if server has intro videos setup and available
introVideos = GetIntroVideos(video_id)
@ -362,7 +359,6 @@ end function
' Extract array of Transcode Reasons from the content URL
' @returns Array of Strings
function getTranscodeReasons(url as string) as object
regex = CreateObject("roRegex", "&TranscodeReasons=([^&]*)", "")
match = regex.Match(url)
@ -384,19 +380,18 @@ end function
function directPlaySupported(meta as object) as boolean
devinfo = CreateObject("roDeviceInfo")
if meta.json.MediaSources[0] <> invalid and meta.json.MediaSources[0].SupportsDirectPlay = false
if isValid(meta.json.MediaSources[0]) and meta.json.MediaSources[0].SupportsDirectPlay = false
return false
end if
if meta.json.MediaStreams[0] = invalid
if not isValid(meta.json.MediaSources[0])
return false
end if
streamInfo = { Codec: meta.json.MediaStreams[0].codec }
if meta.json.MediaStreams[0].Profile <> invalid and meta.json.MediaStreams[0].Profile.len() > 0
if isValid(meta.json.MediaStreams[0].Profile) and meta.json.MediaStreams[0].Profile.len() > 0
streamInfo.Profile = LCase(meta.json.MediaStreams[0].Profile)
end if
if meta.json.MediaSources[0].container <> invalid and meta.json.MediaSources[0].container.len() > 0
if isValid(meta.json.MediaSources[0].container) and meta.json.MediaSources[0].container.len() > 0
'CanDecodeVideo() requires the .container to be format: “mp4”, “hls”, “mkv”, “ism”, “dash”, “ts” if its to direct stream
if meta.json.MediaSources[0].container = "mov"
streamInfo.Container = "mp4"
@ -406,8 +401,7 @@ function directPlaySupported(meta as object) as boolean
end if
decodeResult = devinfo.CanDecodeVideo(streamInfo)
return decodeResult <> invalid and decodeResult.result
return isValid(decodeResult) and decodeResult.result
end function
function getContainerType(meta as object) as string
@ -455,12 +449,12 @@ sub autoPlayNextEpisode(videoID as string, showID as string)
resp = APIRequest(url, urlParams)
data = getJson(resp)
if data <> invalid and data.Items.Count() = 2
if isValid(data) and data.Items.Count() = 2
' setup new video node
nextVideo = CreateVideoPlayerGroup(data.Items[1].Id, invalid, 1, false, false)
' remove last videoplayer scene
m.global.sceneManager.callFunc("clearPreviousScene")
if nextVideo <> invalid
if isValid(nextVideo)
m.global.sceneManager.callFunc("pushScene", nextVideo)
else
m.global.sceneManager.callFunc("popScene")
@ -478,7 +472,7 @@ end sub
' In the future, with a custom playback info view, we can return an associated array.
function GetPlaybackInfo()
sessions = api_API().sessions.get()
if sessions <> invalid and sessions.Count() > 0
if isValid(sessions) and sessions.Count() > 0
return GetTranscodingStats(sessions[0])
end if

View File

@ -38,7 +38,6 @@ function searchMedia(query as string)
' This appears to be done differently on the web now
' For each potential type, a separate query is done:
' varying item types, and artists, and people
if query <> ""
resp = APIRequest(Substitute("Search/Hints", get_setting("active_user")), {
"searchTerm": query,
@ -55,7 +54,6 @@ function searchMedia(query as string)
"limit": 100
})
data = getJson(resp)
results = []
for each item in data.SearchHints
@ -79,7 +77,7 @@ function ItemMetaData(id as string)
imgParams = {}
if data.type <> "Audio"
if data?.UserData?.PlayedPercentage <> invalid
if data.UserData <> invalid and data.UserData.PlayedPercentage <> invalid
param = { "PercentPlayed": data.UserData.PlayedPercentage }
imgParams.Append(param)
end if

View File

@ -21,7 +21,7 @@ end function
' returns the server-side track index for the appriate subtitle
function defaultSubtitleTrackFromVid(video_id) as integer
meta = ItemMetaData(video_id)
if meta?.json?.mediaSources <> invalid
if isValid(meta) and isValid(meta.json) and isValid(meta.json.mediaSources)
subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams)
default_text_subs = defaultSubtitleTrack(subtitles["all"], true) ' Find correct subtitle track (forced text)
if default_text_subs <> -1
@ -130,7 +130,7 @@ function selectSubtitleTrackDialog(tracks, currentTrack = -1)
default = ""
if item.IsForced then forced = " [Forced]"
if item.IsDefault then default = " - Default"
if item.Track.Language <> invalid
if isValid(item.Track.Language)
language = iso6392.lookup(item.Track.Language)
if language = invalid then language = item.Track.Language
else
@ -157,7 +157,7 @@ sub changeSubtitleDuringPlayback(newid)
currentSubtitles = video.Subtitles[video.SelectedSubtitle]
newSubtitles = video.Subtitles[newid]
if newSubtitles.IsEncoded or (currentSubtitles <> invalid and currentSubtitles.IsEncoded)
if newSubtitles.IsEncoded or (isValid(currentSubtitles) and currentSubtitles.IsEncoded)
' With encoded subtitles we need to stop/start playback
video.control = "stop"
AddVideoContent(video, video.mediaSourceId, video.audioIndex, newSubtitles.Index, video.position * 10000000)
@ -195,7 +195,7 @@ function sortSubtitles(id as string, MediaStreams)
if stream.type = "Subtitle"
url = ""
if stream.DeliveryUrl <> invalid
if isValid(stream.DeliveryUrl)
url = buildURL(stream.DeliveryUrl)
end if

View File

@ -91,19 +91,23 @@ function get_dialog_result(dialog, port)
end function
function lastFocusedChild(obj as object) as object
if LCase(obj.focusedChild.focusedChild.subType()) = "tvepisodes"
if isValid(obj?.focusedChild?.focusedChild?.lastFocus)
return obj.focusedChild.focusedChild.lastFocus
if isValid(obj)
if isValid(obj.focusedChild) and isValid(obj.focusedChild.focusedChild) and LCase(obj.focusedChild.focusedChild.subType()) = "tvepisodes"
if isValid(obj.focusedChild.focusedChild.lastFocus)
return obj.focusedChild.focusedChild.lastFocus
end if
end if
end if
child = obj
for i = 0 to obj.getChildCount()
if obj.focusedChild <> invalid
child = child.focusedChild
end if
end for
return child
child = obj
for i = 0 to obj.getChildCount()
if isValid(obj.focusedChild)
child = child.focusedChild
end if
end for
return child
else
return invalid
end if
end function
function show_dialog(message as string, options = [], defaultSelection = 0) as integer
@ -282,14 +286,53 @@ function findNodeBySubtype(node, subtype)
return foundNodes
end function
' Search string array for search value. Return if it's found
function inArray(array, searchValue) as boolean
for each item in array
if lcase(item) = lcase(searchValue) then return true
function AssocArrayEqual(Array1 as object, Array2 as object) as boolean
if not isValid(Array1) or not isValid(Array2)
return false
end if
if not Array1.Count() = Array2.Count()
return false
end if
for each key in Array1
if not Array2.DoesExist(key)
return false
end if
if Array1[key] <> Array2[key]
return false
end if
end for
return true
end function
' Search string array for search value. Return if it's found
function inArray(haystack, needle) as boolean
valueToFind = needle
if LCase(type(valueToFind)) <> "rostring" and LCase(type(valueToFind)) <> "string"
valueToFind = str(needle)
end if
valueToFind = lcase(valueToFind)
for each item in haystack
if lcase(item) = valueToFind then return true
end for
return false
end function
function toString(input) as string
if LCase(type(input)) = "rostring" or LCase(type(input)) = "string"
return input
end if
return str(input)
end function
sub startLoadingSpinner()
m.spinner = createObject("roSGNode", "Spinner")
m.spinner.translation = "[900, 450]"
@ -309,7 +352,7 @@ sub stopLoadingSpinner()
if isValid(m.spinner)
m.spinner.visible = false
end if
if isValid(m.scene?.dialog)
if isValid(m.scene) and isValid(m.scene.dialog)
m.scene.dialog.close = true
end if
end sub