Merge branch 'unstable' into V1.6.2

This commit is contained in:
1hitsong 2022-12-11 17:04:14 -05:00
commit 28eabb819f
64 changed files with 4300 additions and 8942 deletions

View File

@ -9,7 +9,7 @@ jobs:
permissions: permissions:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/stale@3de2653986ebd134983c79fe2be5d45cc3d9f4e1 # tag=v6 - uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # tag=v6
with: with:
days-before-issue-stale: -1 days-before-issue-stale: -1
days-before-issue-close: -1 days-before-issue-close: -1
@ -17,4 +17,5 @@ jobs:
close-pr-message: "This pull request has been closed because it has been inactive for 28 days. You may submit a new pull request if desired." close-pr-message: "This pull request has been closed because it has been inactive for 28 days. You may submit a new pull request if desired."
days-before-pr-stale: 21 days-before-pr-stale: 21
days-before-pr-close: 7 days-before-pr-close: 7
exempt-draft-pr: true
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -8,14 +8,14 @@ jobs:
run: run:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3 - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3
- uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3 - uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
with: with:
node-version: "14.12.0" node-version: "14.12.0"
- run: npm ci - run: npm ci
- run: npx ropm install - run: npx ropm install
- run: make dev - run: make dev
- uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3 - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
with: with:
name: Jellyfin-Roku-dev-${{ github.sha }} name: Jellyfin-Roku-dev-${{ github.sha }}
path: ${{ github.workspace }}/out/staging path: ${{ github.workspace }}/out/staging

View File

@ -22,7 +22,7 @@ jobs:
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "MINOR="$2; }' manifest >> $GITHUB_ENV run: awk 'BEGIN { FS="=" } /^minor_version/ { print "MINOR="$2; }' manifest >> $GITHUB_ENV
- name: "Find and save build_version from manifest" - name: "Find and save build_version from manifest"
run: awk 'BEGIN { FS="=" } /^build_version/ { print "BUILD="$2; }' manifest >> $GITHUB_ENV run: awk 'BEGIN { FS="=" } /^build_version/ { print "BUILD="$2; }' manifest >> $GITHUB_ENV
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3 - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3
- uses: vimtor/action-zip@5f1c4aa587ea41db1110df6a99981dbe19cee310 # tag=v1 - uses: vimtor/action-zip@5f1c4aa587ea41db1110df6a99981dbe19cee310 # tag=v1
with: with:
recursive: false recursive: false
@ -36,7 +36,7 @@ jobs:
prerelease: false prerelease: false
title: v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }} title: v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}
files: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip files: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip
- uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3 - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
with: with:
name: jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip name: jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip
path: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip path: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip

View File

@ -22,7 +22,7 @@ jobs:
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "MINOR="$2; }' manifest >> $GITHUB_ENV run: awk 'BEGIN { FS="=" } /^minor_version/ { print "MINOR="$2; }' manifest >> $GITHUB_ENV
- name: "Find and save build_version from manifest" - name: "Find and save build_version from manifest"
run: awk 'BEGIN { FS="=" } /^build_version/ { print "BUILD="$2; }' manifest >> $GITHUB_ENV run: awk 'BEGIN { FS="=" } /^build_version/ { print "BUILD="$2; }' manifest >> $GITHUB_ENV
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3 - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3
- uses: vimtor/action-zip@5f1c4aa587ea41db1110df6a99981dbe19cee310 # tag=v1 - uses: vimtor/action-zip@5f1c4aa587ea41db1110df6a99981dbe19cee310 # tag=v1
with: with:
recursive: false recursive: false
@ -35,7 +35,7 @@ jobs:
prerelease: true prerelease: true
title: v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }} title: v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}
files: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip files: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip
- uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3 - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
with: with:
name: jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip name: jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip
path: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip path: ${{ github.workspace }}/jellyfin_v${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.BUILD }}.zip

5
.gitignore vendored
View File

@ -12,11 +12,6 @@ roku_modules
#NPM modules #NPM modules
node_modules/ node_modules/
#Rooibos generated
rooibosFunctionMap.brs
*/buildinfo.brs
logs
#Eclipse #Eclipse
.buildpath .buildpath
.project .project

View File

@ -130,37 +130,12 @@ Modify code -> `make install` -> Use Roku remote to test changes -> `telnet ${RO
Unfortunately there is no debuger. 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 debuger. 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.
### Testing
Testing is done with the [Rooibos](https://github.com/georgejecook/rooibos/) library. This works by including tests in the deployment and then looking at telnet
for the test results.
Install necessary packages: Install necessary packages:
```bash ```bash
sudo apt-get install nodejs npm sudo apt-get install nodejs npm
``` ```
Install [rooibos-cli](https://github.com/georgejecook/rooibos-cli):
```bash
npm install -g rooibos-cli
```
Deploy the application with tests:
```bash
make test
```
View test results:
```bash
telnet ${ROKU_DEV_TARGET} 8085
```
To exit telnet: `CTRL + ]` and then type `quit + ENTER`
### Committing ### Committing
Before commiting your code, please run: Before commiting your code, please run:

View File

@ -1,8 +1,6 @@
######################################################################### #########################################################################
# Makefile Usage: # Makefile Usage:
# > make test ' run all tests
# > make testFailures ' run all tests and show only failures
# #
# 1) Make sure that you have the curl command line executable in your path # 1) Make sure that you have the curl command line executable in your path
# 2) Set the variable ROKU_DEV_TARGET in your environment to the IP # 2) Set the variable ROKU_DEV_TARGET in your environment to the IP
@ -12,11 +10,9 @@
########################################################################## ##########################################################################
APPNAME = Jellyfin_Roku APPNAME = Jellyfin_Roku
VERSION = 1.6.1 VERSION = 1.6.2
ROKU_TEST_ID = 1
ROKU_TEST_WAIT_DURATION = 5
ZIP_EXCLUDE= -x rooibos/**\* -x xml/* -x artwork/* -x \*.pkg -x storeassets\* -x keys\* -x \*/.\* -x *.git* -x *.DS* -x *.pkg* -x dist/**\* -x out/**\* ZIP_EXCLUDE= -x xml/* -x artwork/* -x \*.pkg -x storeassets\* -x keys\* -x \*/.\* -x *.git* -x *.DS* -x *.pkg* -x dist/**\* -x out/**\*
include app.mk include app.mk
@ -29,7 +25,4 @@ beta:
release: release:
$(MAKE) BUILD='release' package $(MAKE) BUILD='release' package
test: prep_staging prep_tests remove install
echo "Running tests"
deploy: prep_staging remove install deploy: prep_staging remove install

7
app.mk
View File

@ -163,13 +163,6 @@ package: prep_staging
@echo "*** packaging $(APPNAME)-$(BUILD) complete ***" @echo "*** packaging $(APPNAME)-$(BUILD) complete ***"
prep_tests:
@mkdir -p $(STAGINGREL)/components/tests/; \
mkdir -p $(STAGINGREL)/source/tests/; \
cp -r $(SOURCEREL)/tests/components/* $(STAGINGREL)/components/tests/;\
cp -r $(SOURCEREL)/tests/source/* $(STAGINGREL)/source/tests/;\
./node_modules/.bin/rooibos-cli i tests/.rooibosrc.json
prep_commit: prep_commit:
npm run format npm run format
npm ci npm ci

View File

@ -0,0 +1,13 @@
sub init()
m.top.functionName = "getNextEpisodeTask"
end sub
sub getNextEpisodeTask()
m.nextEpisodeData = api_API().shows.getepisodes(m.top.showID, {
UserId: get_setting("active_user"),
StartItemId: m.top.videoID,
Limit: 2
})
m.top.nextEpisodeData = m.nextEpisodeData
end sub

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="GetNextEpisodeTask" extends="Task">
<interface>
<field id="videoID" type="string" />
<field id="showID" type="string" />
<field id="nextEpisodeData" type="assocarray" />
</interface>
<script type="text/brightscript" uri="GetNextEpisodeTask.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

@ -0,0 +1,42 @@
sub init()
m.itemPoster = m.top.findNode("itemPoster")
m.posterText = m.top.findNode("posterText")
m.posterText.font.size = 30
m.backdrop = m.top.findNode("backdrop")
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
'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
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
end if
end sub
sub itemContentChanged()
m.backdrop.blendColor = "#101010"
itemData = m.top.itemContent
if not isValid(itemData) then return
m.itemPoster.uri = itemData.PosterUrl
m.posterText.text = itemData.title
'If Poster not loaded, ensure "blue box" is shown until loaded
if m.itemPoster.loadStatus <> "ready"
m.backdrop.visible = true
m.posterText.visible = true
end if
end sub
'Hide backdrop and text when poster loaded
sub onPosterLoadStatusChanged()
if m.itemPoster.loadStatus = "ready"
m.backdrop.visible = false
m.posterText.visible = false
end if
end sub

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="GridItemSmall" extends="Group">
<children>
<Poster id="backdrop" translation="[0,15]" width="230" height="320" loadDisplayMode="scaleToZoom" uri="pkg:/images/white.9.png" />
<Poster id="itemPoster" translation="[0,15]" width="230" height="320" loadDisplayMode="scaleToZoom" />
<Poster id="itemIcon" width="50" height="50" translation="[230,10]" />
<Label id="posterText" width="230" height="320" translation="[5,5]" horizAlign="center" vertAlign="center" ellipsizeOnBoundary="true" wrap="true" />
</children>
<interface>
<field id="itemContent" type="node" onChange="itemContentChanged" />
<field id="itemHasFocus" type="boolean" onChange="focusChanged" />
</interface>
<script type="text/brightscript" uri="GridItemSmall.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>

View File

@ -320,13 +320,14 @@ end sub
' Set Photo Album view, sort, and filter options ' Set Photo Album view, sort, and filter options
sub setPhotoAlbumOptions(options) sub setPhotoAlbumOptions(options)
' TODO/FIXME: Show shuffle options once implemented options.views = [
' options.views = [ { "Title": tr("Slideshow Off"), "Name": "singlephoto" }
' { "Title": tr("Don't Shuffle"), "Name": "singlephoto"} { "Title": tr("Slideshow On"), "Name": "slideshowphoto" }
' { "Title": tr("Shuffle"), "Name": "shufflephoto"} { "Title": tr("Random Off"), "Name": "singlephoto" }
' ] { "Title": tr("Random On"), "Name": "randomphoto" }
options.views = [] ]
options.sort = [] options.sort = []
options.filter = []
end sub end sub
' Set Default view, sort, and filter options ' Set Default view, sort, and filter options
@ -574,14 +575,17 @@ sub optionsClosed()
end if end if
end if end if
if m.top.parentItem.Type = "CollectionFolder" or m.top.parentItem.CollectionType = "CollectionFolder" if m.top.parentItem.Type = "CollectionFolder" or m.top.parentItem.Type = "Folder" or m.top.parentItem.CollectionType = "CollectionFolder"
' Did the user just request "Shuffle" on a PhotoAlbum? ' Did the user just request "Random" on a PhotoAlbum?
if m.options.view = "singlephoto" if m.options.view = "singlephoto"
' TODO/FIXME: Stop shuffling here set_user_setting("photos.slideshow", "false")
print "TODO/FIXME: Stop any shuffling here" set_user_setting("photos.random", "false")
else if m.options.view = "shufflephoto" else if m.options.view = "slideshowphoto"
' TODO/FIXME: Start shuffling here set_user_setting("photos.slideshow", "true")
print "TODO/FIXME: Start shuffle here" set_user_setting("photos.random", "false")
else if m.options.view = "randomphoto"
set_user_setting("photos.random", "true")
set_user_setting("photos.slideshow", "false")
end if end if
end if end if
@ -724,9 +728,10 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true return true
else if itemToPlay <> invalid and itemToPlay.type = "Photo" else if itemToPlay <> invalid and itemToPlay.type = "Photo"
' Spawn photo player task ' Spawn photo player task
photoPlayer = CreateObject("roSgNode", "PhotoPlayerTask") photoPlayer = CreateObject("roSgNode", "PhotoDetails")
photoPlayer.itemContent = itemToPlay photoPlayer.items = markupGrid
photoPlayer.control = "RUN" photoPlayer.itemIndex = markupGrid.itemFocused
m.global.sceneManager.callfunc("pushScene", photoPlayer)
return true return true
end if end if
else if key = "left" and topGrp.isinFocusChain() else if key = "left" and topGrp.isinFocusChain()

View File

@ -20,6 +20,16 @@ sub loadItems()
sort_order = "Descending" sort_order = "Descending"
end if end if
if m.top.ItemType = "LogoImage"
logoImageExists = api_API().items.headimageurlbyname(m.top.itemId, "logo")
if logoImageExists
m.top.content = [api_API().items.getimageurl(m.top.itemId, "logo", 0, { "maxHeight": 500, "maxWidth": 500, "quality": "90" })]
else
m.top.content = []
end if
return
end if
params = { params = {
limit: m.top.limit, limit: m.top.limit,
@ -52,14 +62,17 @@ sub loadItems()
end if end if
end if end if
if m.top.searchTerm <> "" 'reset data
if LCase(m.top.searchTerm) = LCase(tr("all"))
params.searchTerm = " "
else if m.top.searchTerm <> ""
params.searchTerm = m.top.searchTerm params.searchTerm = m.top.searchTerm
end if end if
filter = m.top.filter filter = LCase(m.top.filter)
if filter = "All" or filter = "all" if filter = "all"
' do nothing ' do nothing
else if filter = "Favorites" else if filter = "favorites"
params.append({ Filters: "IsFavorite" }) params.append({ Filters: "IsFavorite" })
params.append({ isFavorite: true }) params.append({ isFavorite: true })
end if end if
@ -117,7 +130,49 @@ sub loadItems()
else if item.type = "Episode" else if item.type = "Episode"
tmp = CreateObject("roSGNode", "TVEpisode") tmp = CreateObject("roSGNode", "TVEpisode")
else if item.Type = "Genre" else if item.Type = "Genre"
tmp = CreateObject("roSGNode", "FolderData") tmp = CreateObject("roSGNode", "ContentNode")
tmp.title = item.name
genreData = api_API().users.getitemsbyquery(get_setting("active_user"), {
SortBy: "Random",
SortOrder: "Ascending",
IncludeItemTypes: "Movie",
Recursive: true,
Fields: "PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo",
ImageTypeLimit: 1,
EnableImageTypes: "Primary",
Limit: 6,
GenreIds: item.id,
EnableTotalRecordCount: false,
ParentId: m.top.itemId
})
if genreData.Items.Count() > 5
' Add View All item to the start of the row
row = tmp.createChild("FolderData")
row.parentFolder = m.top.itemId
genreMovieImage = api_API().items.getimageurl(item.id)
row.title = item.name
row.json = item
row.FHDPOSTERURL = genreMovieImage
row.HDPOSTERURL = genreMovieImage
row.SDPOSTERURL = genreMovieImage
row.type = "Folder"
end if
for each genreMovie in genreData.Items
row = tmp.createChild("MovieData")
genreMovieImage = api_API().items.getimageurl(genreMovie.id)
row.title = genreMovie.name
row.FHDPOSTERURL = genreMovieImage
row.HDPOSTERURL = genreMovieImage
row.SDPOSTERURL = genreMovieImage
row.json = genreMovie
row.id = genreMovie.id
row.type = genreMovie.type
end for
else if item.Type = "Studio" else if item.Type = "Studio"
tmp = CreateObject("roSGNode", "FolderData") tmp = CreateObject("roSGNode", "FolderData")
else if item.Type = "MusicAlbum" else if item.Type = "MusicAlbum"
@ -135,12 +190,16 @@ sub loadItems()
else else
print "[LoadItems] Unknown Type: " item.Type print "[LoadItems] Unknown Type: " item.Type
end if end if
if tmp <> invalid if tmp <> invalid
if item.Type <> "Genre"
tmp.parentFolder = m.top.itemId tmp.parentFolder = m.top.itemId
tmp.json = item tmp.json = item
if item.UserData <> invalid and item.UserData.isFavorite <> invalid if item.UserData <> invalid and item.UserData.isFavorite <> invalid
tmp.favorite = item.UserData.isFavorite tmp.favorite = item.UserData.isFavorite
end if end if
end if
results.push(tmp) results.push(tmp)
end if end if
end for end for

View File

@ -0,0 +1,790 @@
sub setupNodes()
m.options = m.top.findNode("options")
m.itemGrid = m.top.findNode("itemGrid")
m.voiceBox = m.top.findNode("voiceBox")
m.backdrop = m.top.findNode("backdrop")
m.newBackdrop = m.top.findNode("backdropTransition")
m.emptyText = m.top.findNode("emptyText")
m.selectedMovieName = m.top.findNode("selectedMovieName")
m.selectedMovieOverview = m.top.findNode("selectedMovieOverview")
m.selectedMovieProductionYear = m.top.findNode("selectedMovieProductionYear")
m.selectedMovieOfficialRating = m.top.findNode("selectedMovieOfficialRating")
m.movieLogo = m.top.findNode("movieLogo")
m.swapAnimation = m.top.findNode("backroundSwapAnimation")
m.spinner = m.top.findNode("spinner")
m.Alpha = m.top.findNode("AlphaMenu")
m.AlphaSelected = m.top.findNode("AlphaSelected")
m.micButton = m.top.findNode("micButton")
m.micButtonText = m.top.findNode("micButtonText")
m.communityRatingGroup = m.top.findNode("communityRatingGroup")
m.criticRatingIcon = m.top.findNode("criticRatingIcon")
m.criticRatingGroup = m.top.findNode("criticRatingGroup")
m.overhang = m.top.getScene().findNode("overhang")
m.genreList = m.top.findNode("genrelist")
m.infoGroup = m.top.findNode("infoGroup")
end sub
sub init()
setupNodes()
m.overhang.isVisible = false
m.showItemCount = get_user_setting("itemgrid.showItemCount") = "true"
m.swapAnimation.observeField("state", "swapDone")
m.loadedRows = 0
m.loadedItems = 0
m.data = CreateObject("roSGNode", "ContentNode")
m.itemGrid.content = m.data
m.genreData = CreateObject("roSGNode", "ContentNode")
m.genreList.observeField("itemSelected", "onGenreItemSelected")
m.genreList.content = m.genreData
m.itemGrid.observeField("itemFocused", "onItemFocused")
m.itemGrid.observeField("itemSelected", "onItemSelected")
m.itemGrid.observeField("alphaSelected", "onItemalphaSelected")
'Voice filter setup
m.voiceBox.voiceEnabled = true
m.voiceBox.active = true
m.voiceBox.observeField("text", "onvoiceFilter")
'set voice help text
m.voiceBox.hintText = tr("Use voice remote to search")
'backdrop
m.newBackdrop.observeField("loadStatus", "newBGLoaded")
'Background Image Queued for loading
m.queuedBGUri = ""
'Item sort - maybe load defaults from user prefs?
m.sortField = "SortName"
m.sortAscending = true
m.filter = "All"
m.favorite = "Favorite"
m.loadItemsTask = createObject("roSGNode", "LoadItemsTask2")
m.loadLogoTask = createObject("roSGNode", "LoadItemsTask2")
'set inital counts for overhang before content is loaded.
m.loadItemsTask.totalRecordCount = 0
m.spinner.visible = true
'Get reset folder setting
m.resetGrid = get_user_setting("itemgrid.reset") = "true"
'Check if device has voice remote
devinfo = CreateObject("roDeviceInfo")
'Hide voice search if device does not have voice remote
if devinfo.HasFeature("voice_remote") = false
m.micButton.visible = false
m.micButtonText.visible = false
end if
end sub
sub OnScreenHidden()
if not m.overhang.isVisible
m.overhang.disableMoveAnimation = true
m.overhang.isVisible = true
m.overhang.disableMoveAnimation = false
end if
end sub
sub OnScreenShown()
m.overhang.isVisible = false
if m.top.lastFocus <> invalid
m.top.lastFocus.setFocus(true)
else
m.top.setFocus(true)
end if
end sub
'
'Load initial set of Data
sub loadInitialItems()
m.loadItemsTask.control = "stop"
m.spinner.visible = true
if m.top.parentItem.json.Type = "CollectionFolder"
m.top.HomeLibraryItem = m.top.parentItem.Id
end if
if m.top.parentItem.backdropUrl <> invalid
SetBackground(m.top.parentItem.backdropUrl)
else
SetBackground("")
end if
m.sortField = get_user_setting("display." + m.top.parentItem.Id + ".sortField")
sortAscendingStr = get_user_setting("display." + m.top.parentItem.Id + ".sortAscending")
m.filter = get_user_setting("display." + m.top.parentItem.Id + ".filter")
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
if not isValid(m.sortField) then m.sortField = "SortName"
if not isValid(m.filter) then m.filter = "All"
if not isValid(m.view) then m.view = "Movies"
if sortAscendingStr = invalid or sortAscendingStr = "true"
m.sortAscending = true
else
m.sortAscending = false
end if
if m.top.parentItem.json.type = "Studio"
m.loadItemsTask.studioIds = m.top.parentItem.id
m.loadItemsTask.itemId = m.top.parentItem.parentFolder
m.loadItemsTask.genreIds = ""
else if m.top.parentItem.json.type = "Genre"
m.loadItemsTask.genreIds = m.top.parentItem.id
m.loadItemsTask.itemId = m.top.parentItem.parentFolder
m.loadItemsTask.studioIds = ""
else if m.view = "Movies" or m.options.view = "Movies"
m.loadItemsTask.studioIds = ""
m.loadItemsTask.genreIds = ""
else
m.loadItemsTask.itemId = m.top.parentItem.Id
end if
m.loadItemsTask.nameStartsWith = m.top.alphaSelected
m.loadItemsTask.searchTerm = m.voiceBox.text
m.emptyText.visible = false
m.loadItemsTask.sortField = m.sortField
m.loadItemsTask.sortAscending = m.sortAscending
m.loadItemsTask.filter = m.filter
m.loadItemsTask.startIndex = 0
' Load Item Types
if getCollectionType() = "movies"
m.loadItemsTask.itemType = "Movie"
m.loadItemsTask.itemId = m.top.parentItem.Id
end if
' By default we load movies
m.loadItemsTask.studioIds = ""
m.loadItemsTask.view = "Movies"
m.itemGrid.translation = "[96, 650]"
m.itemGrid.numRows = "2"
m.selectedMovieOverview.visible = true
m.infoGroup.visible = true
if m.options.view = "Studios" or m.view = "Studios"
m.itemGrid.translation = "[96, 60]"
m.itemGrid.numRows = "3"
m.loadItemsTask.view = "Networks"
m.top.imageDisplayMode = "scaleToFit"
m.selectedMovieOverview.visible = false
m.infoGroup.visible = false
else if m.options.view = "Genres" or m.view = "Genres"
m.loadItemsTask.StudioIds = m.top.parentItem.Id
m.loadItemsTask.view = "Genres"
m.movieLogo.visible = false
m.selectedMovieName.visible = false
m.selectedMovieOverview.visible = false
m.infoGroup.visible = false
end if
m.loadItemsTask.observeField("content", "ItemDataLoaded")
m.spinner.visible = true
m.loadItemsTask.control = "RUN"
SetUpOptions()
end sub
' Set Movies view, sort, and filter options
sub setMoviesOptions(options)
options.views = [
{ "Title": tr("Movies"), "Name": "Movies" },
{ "Title": tr("Studios"), "Name": "Studios" },
{ "Title": tr("Genres"), "Name": "Genres" }
]
if m.top.parentItem.json.type = "Genre"
options.views = [
{ "Title": tr("Movies"), "Name": "Movies" }
]
end if
options.sort = [
{ "Title": tr("TITLE"), "Name": "SortName" },
{ "Title": tr("IMDB_RATING"), "Name": "CommunityRating" },
{ "Title": tr("CRITIC_RATING"), "Name": "CriticRating" },
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
{ "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
{ "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
{ "Title": tr("PLAY_COUNT"), "Name": "PlayCount" },
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
{ "Title": tr("RUNTIME"), "Name": "Runtime" }
]
options.filter = [
{ "Title": tr("All"), "Name": "All" },
{ "Title": tr("Favorites"), "Name": "Favorites" }
]
if m.options.view = "Genres" or m.view = "Genres"
options.sort = []
options.filter = []
end if
if m.options.view = "Studios" or m.view = "Studios"
options.sort = [
{ "Title": tr("TITLE"), "Name": "SortName" },
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
]
end if
end sub
' Return parent collection type
function getCollectionType() as string
if m.top.parentItem.collectionType = invalid
return m.top.parentItem.Type
else
return m.top.parentItem.CollectionType
end if
end function
' Search string array for search value. Return if it's found
function inStringArray(array, searchValue) as boolean
for each item in array
if lcase(item) = lcase(searchValue) then return true
end for
return false
end function
' Data to display when options button selected
sub SetUpOptions()
options = {}
options.filter = []
options.favorite = []
setMoviesOptions(options)
' Set selected view option
for each o in options.views
if o.Name = m.view
o.Selected = true
o.Ascending = m.sortAscending
m.options.view = o.Name
end if
end for
' Set selected sort option
for each o in options.sort
if o.Name = m.sortField
o.Selected = true
o.Ascending = m.sortAscending
m.options.sortField = o.Name
end if
end for
' Set selected filter option
for each o in options.filter
if o.Name = m.filter
o.Selected = true
m.options.filter = o.Name
end if
end for
m.options.options = options
end sub
'
' Logo Image Loaded Event Handler
sub LogoImageLoaded(msg)
data = msg.GetData()
m.loadLogoTask.unobserveField("content")
m.loadLogoTask.content = []
if data.Count() > 0
m.movieLogo.uri = data[0]
m.movieLogo.visible = true
else
m.selectedMovieName.visible = true
end if
end sub
'
'Handle loaded data, and add to Grid
sub ItemDataLoaded(msg)
m.top.alphaActive = false
itemData = msg.GetData()
m.loadItemsTask.unobserveField("content")
m.loadItemsTask.content = []
if itemData = invalid
m.Loading = false
return
end if
if m.loadItemsTask.view = "Genres"
' Reset genre list data
m.genreData.removeChildren(m.genreData.getChildren(-1, 0))
for each item in itemData
m.genreData.appendChild(item)
end for
m.itemGrid.opacity = "0"
m.genreList.opacity = "1"
m.itemGrid.setFocus(false)
m.genreList.setFocus(true)
m.loading = false
m.spinner.visible = false
return
end if
m.itemGrid.opacity = "1"
m.genreList.opacity = "0"
m.itemGrid.setFocus(true)
m.genreList.setFocus(false)
for each item in itemData
m.data.appendChild(item)
end for
'Update the stored counts
m.loadedItems = m.itemGrid.content.getChildCount()
m.loadedRows = m.loadedItems / m.itemGrid.numColumns
m.Loading = false
'If there are no items to display, show message
if m.loadedItems = 0
m.emptyText.text = tr("NO_ITEMS").Replace("%1", m.top.parentItem.Type)
m.emptyText.visible = true
end if
m.spinner.visible = false
end sub
'
'Set Selected Movie Name
sub SetName(movieName as string)
m.selectedMovieName.text = movieName
end sub
'
'Set Selected Movie Overview
sub SetOverview(movieOverview as string)
m.selectedMovieOverview.text = movieOverview
end sub
'
'Set Selected Movie OfficialRating
sub SetOfficialRating(movieOfficialRating as string)
m.selectedMovieOfficialRating.text = movieOfficialRating
end sub
'
'Set Selected Movie ProductionYear
sub SetProductionYear(movieProductionYear)
m.selectedMovieProductionYear.text = movieProductionYear
end sub
'
'Set Background Image
sub SetBackground(backgroundUri as string)
if backgroundUri = ""
m.backdrop.opacity = 0
end if
'If a new image is being loaded, or transitioned to, store URL to load next
if m.swapAnimation.state <> "stopped" or m.newBackdrop.loadStatus = "loading"
m.queuedBGUri = backgroundUri
return
end if
m.newBackdrop.uri = backgroundUri
end sub
'
'Handle new item being focused
sub onItemFocused()
focusedRow = m.itemGrid.currFocusRow
itemInt = m.itemGrid.itemFocused
' If no selected item, set background to parent backdrop
if itemInt = -1
return
end if
m.movieLogo.visible = false
m.selectedMovieName.visible = false
' Load more data if focus is within last 5 rows, and there are more items to load
if focusedRow >= m.loadedRows - 5 and m.loadeditems < m.loadItemsTask.totalRecordCount
loadMoreData()
end if
m.selectedFavoriteItem = getItemFocused()
m.communityRatingGroup.visible = false
m.criticRatingGroup.visible = false
if m.options.view = "Studios" or m.view = "Studios"
return
end if
itemData = m.selectedFavoriteItem.json
if isValid(itemData.communityRating)
setFieldText("communityRating", int(itemData.communityRating * 10) / 10)
m.communityRatingGroup.visible = true
end if
if isValid(itemData.CriticRating)
setFieldText("criticRatingLabel", itemData.criticRating)
tomato = "pkg:/images/rotten.png"
if itemData.CriticRating > 60
tomato = "pkg:/images/fresh.png"
end if
m.criticRatingIcon.uri = tomato
m.criticRatingGroup.visible = true
end if
if isValid(itemData.Name)
SetName(itemData.Name)
else
SetName("")
end if
if isValid(itemData.Overview)
SetOverview(itemData.Overview)
else
SetOverview("")
end if
if isValid(itemData.ProductionYear)
SetProductionYear(str(itemData.ProductionYear))
else
SetProductionYear("")
end if
if type(itemData.RunTimeTicks) = "LongInteger"
setFieldText("runtime", stri(getRuntime(itemData.RunTimeTicks)) + " mins")
else
setFieldText("runtime", "")
end if
if isValid(itemData.OfficialRating)
SetOfficialRating(itemData.OfficialRating)
else
SetOfficialRating("")
end if
m.loadLogoTask.itemId = itemData.id
m.loadLogoTask.itemType = "LogoImage"
m.loadLogoTask.observeField("content", "LogoImageLoaded")
m.loadLogoTask.control = "RUN"
' Set Background to item backdrop
SetBackground(m.selectedFavoriteItem.backdropUrl)
end sub
function getRuntime(runTimeTicks) as integer
return round(runTimeTicks / 600000000.0)
end function
function round(f as float) as integer
' BrightScript only has a "floor" round
' This compares floor to floor + 1 to find which is closer
m = int(f)
n = m + 1
x = abs(f - m)
y = abs(f - n)
if y > x
return m
else
return n
end if
end function
sub setFieldText(field, value)
node = m.top.findNode(field)
if node = invalid or value = invalid then return
' Handle non strings... Which _shouldn't_ happen, but hey
if type(value) = "roInt" or type(value) = "Integer"
value = str(value)
else if type(value) = "roFloat" or type(value) = "Float"
value = str(value)
else if type(value) <> "roString" and type(value) <> "String"
value = ""
end if
node.text = value
end sub
'
'When Image Loading Status changes
sub newBGLoaded()
'If image load was sucessful, start the fade swap
if m.newBackdrop.loadStatus = "ready"
m.swapAnimation.control = "start"
end if
end sub
'
'Swap Complete
sub swapDone()
if m.swapAnimation.state = "stopped"
'Set main BG node image and hide transitioning node
m.backdrop.uri = m.newBackdrop.uri
m.backdrop.opacity = 1
m.newBackdrop.opacity = 0
'If there is another one to load
if m.newBackdrop.uri <> m.queuedBGUri and m.queuedBGUri <> ""
SetBackground(m.queuedBGUri)
m.queuedBGUri = ""
end if
end if
end sub
'
'Load next set of items
sub loadMoreData()
m.spinner.visible = true
if m.Loading = true then return
m.Loading = true
m.loadItemsTask.startIndex = m.loadedItems
m.loadItemsTask.observeField("content", "ItemDataLoaded")
m.loadItemsTask.control = "RUN"
end sub
'
'Item Selected
sub onItemSelected()
m.top.selectedItem = m.itemGrid.content.getChild(m.itemGrid.itemSelected)
end sub
'
'Returns Focused Item
function getItemFocused()
return m.itemGrid.content.getChild(m.itemGrid.itemFocused)
end function
'
'Genre Item Selected
sub onGenreItemSelected()
m.top.selectedItem = m.genreList.content.getChild(m.genreList.rowItemSelected[0]).getChild(m.genreList.rowItemSelected[1])
end sub
sub onItemalphaSelected()
if m.top.alphaSelected <> ""
m.loadedRows = 0
m.loadedItems = 0
m.data = CreateObject("roSGNode", "ContentNode")
m.itemGrid.content = m.data
m.genreData = CreateObject("roSGNode", "ContentNode")
m.genreList.content = m.genreData
m.loadItemsTask.searchTerm = ""
m.VoiceBox.text = ""
m.loadItemsTask.nameStartsWith = m.alpha.itemAlphaSelected
m.spinner.visible = true
loadInitialItems()
end if
end sub
sub onvoiceFilter()
if m.VoiceBox.text <> ""
m.loadedRows = 0
m.loadedItems = 0
m.data = CreateObject("roSGNode", "ContentNode")
m.itemGrid.content = m.data
m.top.alphaSelected = ""
m.loadItemsTask.NameStartsWith = " "
m.loadItemsTask.searchTerm = m.voiceBox.text
m.loadItemsTask.recursive = true
m.spinner.visible = true
loadInitialItems()
end if
end sub
'
'Check if options updated and any reloading required
sub optionsClosed()
reload = false
if m.options.sortField <> m.sortField or m.options.sortAscending <> m.sortAscending
m.sortField = m.options.sortField
m.sortAscending = m.options.sortAscending
reload = true
sortAscendingStr = "true"
'Store sort settings
if not m.sortAscending
sortAscendingStr = "false"
end if
set_user_setting("display." + m.top.parentItem.Id + ".sortField", m.sortField)
set_user_setting("display." + m.top.parentItem.Id + ".sortAscending", sortAscendingStr)
end if
if m.options.filter <> m.filter
m.filter = m.options.filter
reload = true
set_user_setting("display." + m.top.parentItem.Id + ".filter", m.options.filter)
end if
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
if m.options.view <> m.view
m.view = m.options.view
set_user_setting("display." + m.top.parentItem.Id + ".landing", m.view)
' Reset any filtering or search terms
m.top.alphaSelected = ""
m.loadItemsTask.NameStartsWith = " "
m.loadItemsTask.searchTerm = ""
m.filter = "All"
m.sortField = "SortName"
m.sortAscending = true
' Reset view to defaults
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)
reload = true
end if
if reload
m.loadedRows = 0
m.loadedItems = 0
m.data = CreateObject("roSGNode", "ContentNode")
m.itemGrid.content = m.data
loadInitialItems()
end if
m.itemGrid.setFocus(m.itemGrid.opacity = 1)
m.genreList.setFocus(m.genreList.opacity = 1)
end sub
sub onChannelSelected(msg)
node = msg.getRoSGNode()
m.top.lastFocus = lastFocusedChild(node)
if node.watchChannel <> invalid
' Clone the node when it's reused/update in the TimeGrid it doesn't automatically start playing
m.top.selectedItem = node.watchChannel.clone(false)
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
if key = "left" and m.voiceBox.isinFocusChain()
m.itemGrid.setFocus(m.itemGrid.opacity = 1)
m.genreList.setFocus(m.genreList.opacity = 1)
m.voiceBox.setFocus(false)
end if
if key = "options"
if m.options.visible = true
m.options.visible = false
m.top.removeChild(m.options)
optionsClosed()
else
itemSelected = m.selectedFavoriteItem
if itemSelected <> invalid
m.options.selectedFavoriteItem = itemSelected
end if
m.options.visible = true
m.top.appendChild(m.options)
m.options.setFocus(true)
end if
return true
else if key = "back"
if m.options.visible = true
m.options.visible = false
optionsClosed()
return true
else
m.global.sceneManager.callfunc("popScene")
m.loadItemsTask.control = "stop"
return true
end if
else if key = "play" or key = "OK"
itemToPlay = getItemFocused()
if itemToPlay <> invalid and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode")
m.top.quickPlayNode = itemToPlay
return true
end if
else if key = "left"
if m.itemGrid.isinFocusChain()
m.top.alphaActive = true
m.itemGrid.setFocus(false)
alpha = m.alpha.getChild(0).findNode("Alphamenu")
alpha.setFocus(true)
return true
else if m.genreList.isinFocusChain()
m.top.alphaActive = true
m.genreList.setFocus(false)
alpha = m.alpha.getChild(0).findNode("Alphamenu")
alpha.setFocus(true)
return true
end if
else if key = "right" and m.Alpha.isinFocusChain()
m.top.alphaActive = false
m.Alpha.setFocus(false)
m.Alpha.visible = true
m.itemGrid.setFocus(m.itemGrid.opacity = 1)
m.genreList.setFocus(m.genreList.opacity = 1)
return true
else if key = "replay" and m.itemGrid.isinFocusChain()
if m.resetGrid = true
m.itemGrid.animateToItem = 0
else
m.itemGrid.jumpToItem = 0
end if
else if key = "replay" and m.genreList.isinFocusChain()
if m.resetGrid = true
m.genreList.animateToItem = 0
else
m.genreList.jumpToItem = 0
end if
return true
end if
if key = "replay"
m.spinner.visible = true
m.loadItemsTask.searchTerm = ""
m.loadItemsTask.nameStartsWith = ""
m.voiceBox.text = ""
m.top.alphaSelected = ""
m.loadItemsTask.filter = "All"
m.filter = "All"
m.data = CreateObject("roSGNode", "ContentNode")
m.itemGrid.content = m.data
loadInitialItems()
return true
end if
return false
end function

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="MovieLibraryView" extends="JFScreen">
<children>
<Rectangle id="screenSaverBackground" width="1920" height="1080" color="#000000" />
<VoiceTextEditBox id="VoiceBox" visible="true" width = "40" translation = "[52, 120]" />
<Rectangle id="VoiceBoxCover" height="240" width="100" color="0x000000ff" translation = "[25, 75]" />
<maskGroup translation="[820, 0]" id="backgroundMask" maskUri="pkg:/images/backgroundmask.png" maskSize="[1220,700]">
<poster id="backdrop" loadDisplayMode="scaleToFill" width="1100" height="700" opacity="1" />
<poster id="backdropTransition" loadDisplayMode="scaleToFill" width="1100" height="700" opacity="1" />
</maskGroup>
<Label id="selectedMovieName" visible="false" translation="[120, 40]" wrap="true" font="font:LargeBoldSystemFont" width="850" height="196" horizAlign="left" vertAlign="center" />
<Poster id="movieLogo" visible="false" translation="[120, 40]" loadDisplayMode="scaleToFit" width="384" height="196" />
<LayoutGroup layoutDirection="horiz" translation="[120, 270]" itemSpacings="[30]" id="infoGroup">
<Label id="selectedMovieProductionYear" font="font:SmallestSystemFont" />
<Label id="runtime" font="font:SmallestSystemFont" />
<Label id="selectedMovieOfficialRating" font="font:SmallestSystemFont" />
<LayoutGroup id="communityRatingGroup" visible="false" layoutDirection="horiz" itemSpacings="[-5]">
<Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="28" width="28" blendColor="#00a4dcFF" />
<Label id="communityRating" font="font:SmallestSystemFont" />
</LayoutGroup>
<LayoutGroup layoutDirection="horiz" id="criticRatingGroup">
<Poster id="criticRatingIcon" height="28" width="28" />
<Label id="criticRatingLabel" font="font:SmallestSystemFont" />
</LayoutGroup>
</LayoutGroup>
<Label id="selectedMovieOverview" font="font:SmallestSystemFont" translation="[120, 360]" wrap="true" lineSpacing="20" maxLines="5" width="850" ellipsisText="..." />
<MarkupGrid id="itemGrid" itemComponentName="GridItemSmall" numColumns="7" numRows="2" vertFocusAnimationStyle="fixed" itemSize="[230, 310]" itemSpacing="[20, 20]" />
<RowList opacity="0" id="genrelist" translation="[120, 60]" showRowLabel="true" itemComponentName="GridItemSmall" numColumns="1" numRows="3" vertFocusAnimationStyle="fixed" itemSize = "[1900, 360]" rowItemSize="[ [230, 320] ]" rowItemSpacing="[ [20, 0] ]" itemSpacing="[0, 60]" />
<Label id="micButtonText" font="font:SmallSystemFont" visible="false" />
<Button id = "micButton" maxWidth = "20" translation = "[20, 120]" iconUri = "pkg:/images/icons/mic_icon.png"/>
<Label translation="[0,540]" id="emptyText" font="font:LargeSystemFont" width="1910" horizAlign="center" vertAlign="center" height="64" visible="false" />
<ItemGridOptions id="options" visible="false" />
<Spinner id="spinner" translation="[900, 450]" />
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear">
<FloatFieldInterpolator id = "fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 1.00 ]" fieldToInterp="backdropTransition.opacity" />
<FloatFieldInterpolator id = "fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 1.00, 0.00 ]" fieldToInterp="backdrop.opacity" />
</Animation>
<Alpha id="AlphaMenu" />
</children>
<interface>
<field id="HomeLibraryItem" type="string"/>
<field id="parentItem" type="node" onChange="loadInitialItems" />
<field id="selectedItem" type="node" alwaysNotify="true" />
<field id="quickPlayNode" type="node" alwaysNotify="true" />
<field id="imageDisplayMode" type="string" value="scaleToZoom" />
<field id="AlphaSelected" type="string" alias="AlphaMenu.itemAlphaSelected" alwaysNotify="true" onChange="onItemAlphaSelected" />
<field id="alphaActive" type="boolean" value="false" />
<field id="jumpToItem" type="integer" value="" />
</interface>
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<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" />
</component>

View File

@ -25,19 +25,14 @@ sub init()
' get system preference clock format (12/24hr) ' get system preference clock format (12/24hr)
di = CreateObject("roDeviceInfo") di = CreateObject("roDeviceInfo")
m.clockFormat = di.GetClockFormat() m.clockFormat = di.GetClockFormat()
m.overlayHours = m.top.findNode("overlayHours")
' grab current time m.overlayMinutes = m.top.findNode("overlayMinutes")
currentTime = CreateObject("roDateTime") m.overlayMeridian = m.top.findNode("overlayMeridian")
currentTime.ToLocalTime()
m.currentHours = currentTime.GetHours()
m.currentMinutes = currentTime.GetMinutes()
' start timer ' start timer
m.currentTimeTimer = m.top.findNode("currentTimeTimer") m.currentTimeTimer = m.top.findNode("currentTimeTimer")
m.currentTimeTimer.control = "start" m.currentTimeTimer.control = "start"
m.currentTimeTimer.ObserveField("fire", "updateTime") m.currentTimeTimer.ObserveField("fire", "updateTime")
updateTimeDisplay()
end if end if
setClockVisibility() setClockVisibility()
@ -97,64 +92,50 @@ sub updateUser()
end sub end sub
sub updateTime() sub updateTime()
if (m.currentMinutes + 1) > 59 m.currentTime = CreateObject("roDateTime")
m.currentHours = m.currentHours + 1 m.currentTime.ToLocalTime()
m.currentMinutes = 0 m.currentTimeTimer.duration = 60 - m.currentTime.GetSeconds()
else m.currentHours = m.currentTime.GetHours()
m.currentMinutes = m.currentMinutes + 1 m.currentMinutes = m.currentTime.GetMinutes()
end if
updateTimeDisplay() updateTimeDisplay()
end sub end sub
sub resetTime() sub resetTime()
m.currentTimeTimer.control = "stop" m.currentTimeTimer.control = "stop"
currentTime = CreateObject("roDateTime")
m.currentTimeTimer.control = "start" m.currentTimeTimer.control = "start"
updateTime()
currentTime.ToLocalTime()
m.currentHours = currentTime.GetHours()
m.currentMinutes = currentTime.GetMinutes()
updateTimeDisplay()
end sub end sub
sub updateTimeDisplay() sub updateTimeDisplay()
overlayHours = m.top.findNode("overlayHours")
overlayMinutes = m.top.findNode("overlayMinutes")
overlayMeridian = m.top.findNode("overlayMeridian")
if m.clockFormat = "24h" if m.clockFormat = "24h"
overlayMeridian.text = "" m.overlayMeridian.text = ""
if m.currentHours < 10 if m.currentHours < 10
overlayHours.text = "0" + StrI(m.currentHours).trim() m.overlayHours.text = "0" + StrI(m.currentHours).trim()
else else
overlayHours.text = m.currentHours m.overlayHours.text = m.currentHours
end if end if
else else
if m.currentHours < 12 if m.currentHours < 12
overlayMeridian.text = "AM" m.overlayMeridian.text = "AM"
if m.currentHours = 0 if m.currentHours = 0
overlayHours.text = "12" m.overlayHours.text = "12"
else else
overlayHours.text = m.currentHours m.overlayHours.text = m.currentHours
end if end if
else else
overlayMeridian.text = "PM" m.overlayMeridian.text = "PM"
if m.currentHours = 12 if m.currentHours = 12
overlayHours.text = "12" m.overlayHours.text = "12"
else else
overlayHours.text = m.currentHours - 12 m.overlayHours.text = m.currentHours - 12
end if end if
end if end if
end if end if
if m.currentMinutes < 10 if m.currentMinutes < 10
overlayMinutes.text = "0" + StrI(m.currentMinutes).trim() m.overlayMinutes.text = "0" + StrI(m.currentMinutes).trim()
else else
overlayMinutes.text = m.currentMinutes m.overlayMinutes.text = m.currentMinutes
end if end if
end sub end sub

View File

@ -2,6 +2,8 @@ sub init()
m.playbackTimer = m.top.findNode("playbackTimer") m.playbackTimer = m.top.findNode("playbackTimer")
m.bufferCheckTimer = m.top.findNode("bufferCheckTimer") m.bufferCheckTimer = m.top.findNode("bufferCheckTimer")
m.top.observeField("state", "onState") m.top.observeField("state", "onState")
m.top.observeField("content", "onContentChange")
m.playbackTimer.observeField("fire", "ReportPlayback") m.playbackTimer.observeField("fire", "ReportPlayback")
m.bufferPercentage = 0 ' Track whether content is being loaded m.bufferPercentage = 0 ' Track whether content is being loaded
m.playReported = false m.playReported = false
@ -12,6 +14,88 @@ sub init()
clockNode = findNodeBySubtype(m.top, "clock") clockNode = findNodeBySubtype(m.top, "clock")
if clockNode[0] <> invalid then clockNode[0].parent.removeChild(clockNode[0].node) if clockNode[0] <> invalid then clockNode[0].parent.removeChild(clockNode[0].node)
end if end if
'Play Next Episode button
m.nextEpisodeButton = m.top.findNode("nextEpisode")
m.nextEpisodeButton.text = tr("Next Episode")
m.nextEpisodeButton.setFocus(false)
m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
m.checkedForNextEpisode = false
m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask")
m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded")
end sub
' Event handler for when video content field changes
sub onContentChange()
if not isValid(m.top.content) then return
m.top.observeField("position", "onPositionChanged")
' If video content type is not episode, remove position observer
if m.top.content.contenttype <> 4
m.top.unobserveField("position")
end if
end sub
sub onNextEpisodeDataLoaded()
m.checkedForNextEpisode = true
m.top.observeField("position", "onPositionChanged")
if m.getNextEpisodeTask.nextEpisodeData.Items.count() <> 2
m.top.unobserveField("position")
end if
end sub
'
' Runs Next Episode button animation and sets focus to button
sub showNextEpisodeButton()
if not m.nextEpisodeButton.visible
m.showNextEpisodeButtonAnimation.control = "start"
m.nextEpisodeButton.setFocus(true)
m.nextEpisodeButton.visible = true
end if
end sub
'
'Update count down text
sub updateCount()
m.nextEpisodeButton.text = tr("Next Episode") + " " + Int(m.top.runTime - m.top.position).toStr()
end sub
'
' Runs hide Next Episode button animation and sets focus back to video
sub hideNextEpisodeButton()
m.hideNextEpisodeButtonAnimation.control = "start"
m.nextEpisodeButton.setFocus(false)
m.top.setFocus(true)
end sub
' Checks if we need to display the Next Episode button
sub checkTimeToDisplayNextEpisode()
if int(m.top.position) >= (m.top.runTime - 30)
showNextEpisodeButton()
updateCount()
return
end if
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
m.nextEpisodeButton.visible = false
m.nextEpisodeButton.setFocus(false)
end if
end sub
' When Video Player state changes
sub onPositionChanged()
' Check if dialog is open
m.dialog = m.top.getScene().findNode("dialogBackground")
if not isValid(m.dialog)
checkTimeToDisplayNextEpisode()
end if
end sub end sub
' '
@ -40,6 +124,16 @@ sub onState(msg)
m.top.control = "stop" m.top.control = "stop"
m.top.backPressed = true m.top.backPressed = true
else if m.top.state = "playing" else if m.top.state = "playing"
' Check if next episde is available
if isValid(m.top.showID)
if m.top.showID <> "" and not m.checkedForNextEpisode and m.top.content.contenttype = 4
m.getNextEpisodeTask.showID = m.top.showID
m.getNextEpisodeTask.videoID = m.top.id
m.getNextEpisodeTask.control = "RUN"
end if
end if
if m.playReported = false if m.playReported = false
ReportPlayback("start") ReportPlayback("start")
m.playReported = true m.playReported = true
@ -126,12 +220,24 @@ sub dialogClosed(msg)
sourceNode.close = true sourceNode.close = true
end sub end sub
function onKeyEvent(key as string, press as boolean) as boolean function onKeyEvent(key as string, press as boolean) as boolean
if key = "OK" and m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible
m.top.state = "finished"
hideNextEpisodeButton()
return true
else
'Hide Next Episode Button
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
m.nextEpisodeButton.visible = false
m.nextEpisodeButton.setFocus(false)
m.top.setFocus(true)
end if
end if
if not press then return false if not press then return false
if m.top.Subtitles.count() and key = "down" if key = "down"
m.top.selectSubtitlePressed = true m.top.selectSubtitlePressed = true
return true return true
else if key = "up" else if key = "up"

View File

@ -22,12 +22,24 @@
<field id="videoId" type="string" /> <field id="videoId" type="string" />
<field id="mediaSourceId" type="string" /> <field id="mediaSourceId" type="string" />
<field id="audioIndex" type="integer" /> <field id="audioIndex" type="integer" />
<field id="runTime" type="integer" />
</interface> </interface>
<script type="text/brightscript" uri="JFVideo.brs" /> <script type="text/brightscript" uri="JFVideo.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" /> <script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" /> <script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
<children> <children>
<timer id="playbackTimer" repeat="true" duration="30" /> <timer id="playbackTimer" repeat="true" duration="30" />
<timer id="bufferCheckTimer" repeat="true" /> <timer id="bufferCheckTimer" repeat="true" />
<JFButton id="nextEpisode" opacity="0" textColor="#f0f0f0" focusedTextColor="#202020" focusFootprintBitmapUri="pkg:/images/option-menu-bg.9.png" focusBitmapUri="pkg:/images/white.9.png" translation="[1500, 900]" />
<!--animation for the play next episode button-->
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, .9]" fieldToInterp="nextEpisode.opacity" />
</Animation>
<Animation id="hideNextEpisodeButton" duration=".2" repeat="false" easeFunction="inQuad">
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[.9, 0]" fieldToInterp="nextEpisode.opacity" />
</Animation>
</children> </children>
</component> </component>

View File

@ -8,12 +8,14 @@ sub setFields()
m.top.watched = json.UserData.played m.top.watched = json.UserData.played
m.top.Type = "Movie" m.top.Type = "Movie"
if json.MediaSourceCount <> invalid and json.MediaSourceCount > 1 if isValid(json.MediaSourceCount) and json.MediaSourceCount > 1
if isValid(json.MediaSources)
m.top.mediaSources = [] m.top.mediaSources = []
for each source in json.MediaSources for each source in json.MediaSources
m.top.mediaSources.push(source) m.top.mediaSources.push(source)
end for end for
end if end if
end if
if json.ProductionYear <> invalid if json.ProductionYear <> invalid
m.top.SubTitle = json.ProductionYear m.top.SubTitle = json.ProductionYear
@ -49,10 +51,12 @@ sub setPoster()
end if end if
' Add Backdrop Image ' Add Backdrop Image
if m.top.json.BackdropImageTags <> invalid
if m.top.json.BackdropImageTags[0] <> invalid if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 720, "maxWidth": 1280 } imgParams = { "maxHeight": 720, "maxWidth": 1280 }
m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams) m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
end if end if
end if
end if end if
end sub end sub

View File

@ -12,4 +12,5 @@
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" /> <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/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" /> <script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component> </component>

View File

@ -14,6 +14,9 @@ sub init()
m.SpecialFeaturesTask = CreateObject("roSGNode", "LoadItemsTask") m.SpecialFeaturesTask = CreateObject("roSGNode", "LoadItemsTask")
m.SpecialFeaturesTask.itemsToLoad = "specialfeatures" m.SpecialFeaturesTask.itemsToLoad = "specialfeatures"
m.SpecialFeaturesTask.observeField("content", "onSpecialFeaturesLoaded") m.SpecialFeaturesTask.observeField("content", "onSpecialFeaturesLoaded")
m.LoadAdditionalPartsTask = CreateObject("roSGNode", "LoadItemsTask")
m.LoadAdditionalPartsTask.itemsToLoad = "additionalparts"
m.LoadAdditionalPartsTask.observeField("content", "onAdditionalPartsLoaded")
m.LoadMoviesTask = CreateObject("roSGNode", "LoadItemsTask") m.LoadMoviesTask = CreateObject("roSGNode", "LoadItemsTask")
m.LoadMoviesTask.itemsToLoad = "personMovies" m.LoadMoviesTask.itemsToLoad = "personMovies"
m.LoadShowsTask = CreateObject("roSGNode", "LoadItemsTask") m.LoadShowsTask = CreateObject("roSGNode", "LoadItemsTask")
@ -28,10 +31,11 @@ sub updateSize()
m.top.rowItemSpacing = [36, 36] m.top.rowItemSpacing = [36, 36]
end sub end sub
sub loadPeople(data as object) sub loadParts(data as object)
m.top.parentId = data.id m.top.parentId = data.id
m.LoadPeopleTask.peopleList = data.People m.people = data.People
m.LoadPeopleTask.control = "RUN" m.LoadAdditionalPartsTask.itemId = m.top.parentId
m.LoadAdditionalPartsTask.control = "RUN"
end sub end sub
sub loadPersonVideos(personId) sub loadPersonVideos(personId)
@ -41,12 +45,32 @@ sub loadPersonVideos(personId)
m.LoadMoviesTask.control = "RUN" m.LoadMoviesTask.control = "RUN"
end sub end sub
sub onAdditionalPartsLoaded()
parts = m.LoadAdditionalPartsTask.content
m.LoadAdditionalPartsTask.unobserveField("content")
data = CreateObject("roSGNode", "ContentNode") ' The row Node
m.top.content = data
if parts <> invalid and parts.count() > 0
row = buildRow("Additional Parts", parts, 464)
addRowSize([464, 291])
m.top.content.appendChild(row)
m.top.rowItemSize = [[464, 291]]
else
m.top.rowItemSize = [[234, 396]]
end if
m.top.translation = "[75,10]"
' Load Cast and Crew and everything else...
m.LoadPeopleTask.peopleList = m.people
m.LoadPeopleTask.control = "RUN"
end sub
sub onPeopleLoaded() sub onPeopleLoaded()
people = m.LoadPeopleTask.content people = m.LoadPeopleTask.content
m.loadPeopleTask.unobserveField("content") m.loadPeopleTask.unobserveField("content")
data = CreateObject("roSGNode", "ContentNode") ' The row Node
if people <> invalid and people.count() > 0 if people <> invalid and people.count() > 0
row = data.createChild("ContentNode") row = m.top.content.createChild("ContentNode")
row.Title = tr("Cast & Crew") row.Title = tr("Cast & Crew")
for each person in people 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
@ -58,9 +82,6 @@ sub onPeopleLoaded()
row.appendChild(person) row.appendChild(person)
end for end for
end if end if
m.top.content = data
m.top.translation = "[75,10]"
m.top.rowItemSize = [[234, 396]]
m.LikeThisTask.itemId = m.top.parentId m.LikeThisTask.itemId = m.top.parentId
m.LikeThisTask.control = "RUN" m.LikeThisTask.control = "RUN"
end sub end sub
@ -86,6 +107,7 @@ sub onLikeThisLoaded()
end for end for
addRowSize([234, 396]) addRowSize([234, 396])
end if end if
' Special Features next...
m.SpecialFeaturesTask.itemId = m.top.parentId m.SpecialFeaturesTask.itemId = m.top.parentId
m.SpecialFeaturesTask.control = "RUN" m.SpecialFeaturesTask.control = "RUN"
end sub end sub

View File

@ -5,7 +5,7 @@
<field id="type" type="string" /> <field id="type" type="string" />
<field id="parentId" type="string" /> <field id="parentId" type="string" />
<field id="selectedItem" type="node" alwaysNotify="true" /> <field id="selectedItem" type="node" alwaysNotify="true" />
<function name="loadPeople" /> <function name="loadParts" />
<function name="loadPersonVideos" /> <function name="loadPersonVideos" />
</interface> </interface>
<script type="text/brightscript" uri="ExtrasRowList.brs" /> <script type="text/brightscript" uri="ExtrasRowList.brs" />

View File

@ -133,6 +133,7 @@ sub itemContentChanged()
end if end if
return return
end if end if
if itemData.type = "Series" if itemData.type = "Series"
m.itemText.text = itemData.name m.itemText.text = itemData.name
@ -170,6 +171,34 @@ sub itemContentChanged()
return return
end if end if
if itemData.type = "MusicArtist"
m.itemText.text = itemData.name
m.itemTextExtra.text = itemData.json.AlbumArtist
m.itemPoster.uri = ImageURL(itemData.id)
return
end if
if itemData.type = "Audio"
m.itemText.text = itemData.name
m.itemTextExtra.text = itemData.json.AlbumArtist
m.itemPoster.uri = ImageURL(itemData.id)
return
end if
if itemData.type = "TvChannel"
m.itemText.text = itemData.name
m.itemTextExtra.text = itemData.json.AlbumArtist
m.itemPoster.uri = ImageURL(itemData.id)
return
end if
if itemData.type = "Season"
m.itemText.text = itemData.json.SeriesName
m.itemTextExtra.text = itemData.name
m.itemPoster.uri = ImageURL(itemData.id)
return
end if
print "Unhandled Home Item Type: " + itemData.type print "Unhandled Home Item Type: " + itemData.type
end sub end sub

View File

@ -19,13 +19,19 @@ sub init()
' Load the Libraries from API via task ' Load the Libraries from API via task
m.LoadLibrariesTask = createObject("roSGNode", "LoadItemsTask") m.LoadLibrariesTask = createObject("roSGNode", "LoadItemsTask")
m.LoadLibrariesTask.observeField("content", "onLibrariesLoaded") m.LoadLibrariesTask.observeField("content", "onLibrariesLoaded")
' set up tesk nodes for other rows ' set up tesk nodes for other rows
m.LoadContinueTask = createObject("roSGNode", "LoadItemsTask") m.LoadContinueTask = createObject("roSGNode", "LoadItemsTask")
m.LoadContinueTask.itemsToLoad = "continue" m.LoadContinueTask.itemsToLoad = "continue"
m.LoadNextUpTask = createObject("roSGNode", "LoadItemsTask") m.LoadNextUpTask = createObject("roSGNode", "LoadItemsTask")
m.LoadNextUpTask.itemsToLoad = "nextUp" m.LoadNextUpTask.itemsToLoad = "nextUp"
m.LoadOnNowTask = createObject("roSGNode", "LoadItemsTask") m.LoadOnNowTask = createObject("roSGNode", "LoadItemsTask")
m.LoadOnNowTask.itemsToLoad = "onNow" m.LoadOnNowTask.itemsToLoad = "onNow"
m.LoadFavoritesTask = createObject("roSGNode", "LoadItemsTask")
m.LoadFavoritesTask.itemsToLoad = "favorites"
end sub end sub
sub loadLibraries() sub loadLibraries()
@ -61,20 +67,28 @@ sub onLibrariesLoaded()
continueRow.title = tr("Continue Watching") continueRow.title = tr("Continue Watching")
nextUpRow = content.CreateChild("HomeRow") nextUpRow = content.CreateChild("HomeRow")
nextUpRow.title = tr("Next Up >") nextUpRow.title = tr("Next Up >")
favoritesRow = content.CreateChild("HomeRow")
favoritesRow.title = tr("Favorites")
sizeArray = [ sizeArray = [
[464, 311], ' My Media [464, 311], ' My Media
[464, 331], ' Continue Watching [464, 331], ' Continue Watching
[464, 331] ' Next Up [464, 331], ' Next Up
[464, 331] ' Favorites
] ]
haveLiveTV = false haveLiveTV = false
' validate library data ' validate library data
if m.libraryData <> invalid and m.libraryData.count() > 0 if m.libraryData <> invalid and m.libraryData.count() > 0
userConfig = m.top.userConfig userConfig = m.top.userConfig
' populate My Media row ' populate My Media row
filteredMedia = filterNodeArray(m.libraryData, "id", userConfig.MyMediaExcludes) filteredMedia = filterNodeArray(m.libraryData, "id", userConfig.MyMediaExcludes)
for each item in filteredMedia for each item in filteredMedia
mediaRow.appendChild(item) mediaRow.appendChild(item)
end for end for
' create a "Latest In" row for each library ' create a "Latest In" row for each library
filteredLatest = filterNodeArray(m.libraryData, "id", userConfig.LatestItemsExcludes) filteredLatest = filterNodeArray(m.libraryData, "id", userConfig.LatestItemsExcludes)
for each lib in filteredLatest for each lib in filteredLatest
@ -99,6 +113,10 @@ sub onLibrariesLoaded()
m.LoadContinueTask.observeField("content", "updateContinueItems") m.LoadContinueTask.observeField("content", "updateContinueItems")
m.LoadContinueTask.control = "RUN" m.LoadContinueTask.control = "RUN"
' Load the Favorites Data
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
m.LoadFavoritesTask.control = "RUN"
' If we have Live TV access, load "On Now" data ' If we have Live TV access, load "On Now" data
if haveLiveTV if haveLiveTV
m.LoadOnNowTask.observeField("content", "updateOnNowItems") m.LoadOnNowTask.observeField("content", "updateOnNowItems")
@ -116,6 +134,51 @@ sub updateHomeRows()
m.LoadContinueTask.control = "RUN" m.LoadContinueTask.control = "RUN"
end sub end sub
sub updateFavoritesItems()
itemData = m.LoadFavoritesTask.content
m.LoadFavoritesTask.unobserveField("content")
m.LoadFavoritesTask.content = []
if itemData = invalid then return
homeRows = m.top.content
rowIndex = getRowIndex("Favorites")
if itemData.count() < 1
if rowIndex <> invalid
' remove the row
deleteFromSizeArray(rowIndex)
homeRows.removeChildIndex(rowIndex)
end if
else
' remake row using the new data
row = CreateObject("roSGNode", "HomeRow")
row.title = tr("Favorites")
itemSize = [464, 331]
for each item in itemData
usePoster = true
if lcase(item.type) = "episode" or lcase(item.type) = "audio" or lcase(item.type) = "musicartist"
usePoster = false
end if
item.usePoster = usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
if rowIndex = invalid
' insert new row under "My Media"
updateSizeArray(itemSize, 1)
homeRows.insertChild(row, 1)
else
' replace the old row
homeRows.replaceChild(row, rowIndex)
end if
end if
end sub
sub updateContinueItems() sub updateContinueItems()
itemData = m.LoadContinueTask.content itemData = m.LoadContinueTask.content
m.LoadContinueTask.unobserveField("content") m.LoadContinueTask.unobserveField("content")

View File

@ -100,6 +100,27 @@ sub loadItems()
end if end if
end for end for
else if m.top.itemsToLoad = "favorites"
url = Substitute("Users/{0}/Items", get_setting("active_user"))
params = {}
params["Filters"] = "IsFavorite"
params["Limit"] = 20
params["recursive"] = true
params["sortby"] = "random"
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
else if m.top.itemsToLoad = "onNow" else if m.top.itemsToLoad = "onNow"
url = "LiveTv/Programs/Recommended" url = "LiveTv/Programs/Recommended"
params = {} params = {}
@ -151,6 +172,20 @@ sub loadItems()
tmp.json = specfeat tmp.json = specfeat
end for end for
end if end if
else if m.top.itemsToLoad = "additionalparts"
additionalParts = api_API().videos.getAdditionalParts(m.top.itemId)
if isValid(additionalParts)
for each part in additionalParts.items
tmp = CreateObject("roSGNode", "ExtrasData")
params = {}
params["Tags"] = part.ImageTags.Primary
params["MaxWidth"] = 450
params["MaxHeight"] = 402
tmp.posterURL = ImageUrl(part.Id, "Primary", params)
tmp.json = part
results.push(tmp)
end for
end if
else if m.top.itemsToLoad = "likethis" else if m.top.itemsToLoad = "likethis"
params = { "userId": get_setting("active_user"), "limit": 16 } params = { "userId": get_setting("active_user"), "limit": 16 }
url = Substitute("Items/{0}/Similar", m.top.itemId) url = Substitute("Items/{0}/Similar", m.top.itemId)

View File

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

View File

@ -44,7 +44,14 @@ end sub
'Voice Search set 'Voice Search set
sub channelsearchTermSet() sub channelsearchTermSet()
m.scheduleGrid.jumpToChannel = 0 m.scheduleGrid.jumpToChannel = 0
if m.top.searchTerm <> invalid and m.LoadChannelsTask.searchTerm <> m.top.searchTerm 'Reset filter if user says all
if LCase(m.top.searchTerm) = LCase(tr("all")) or m.LoadChannelsTask.searchTerm = LCase(tr("all"))
m.top.searchTerm = " "
m.LoadChannelsTask.searchTerm = " "
m.spinner.visible = true
m.LoadChannelsTask.control = "RUN"
'filter if the searterm is not invalid
else if m.top.searchTerm <> invalid and LCase(m.LoadChannelsTask.searchTerm) <> LCase(m.top.searchTerm)
if m.LoadChannelsTask.state = "run" then m.LoadChannelsTask.control = "stop" if m.LoadChannelsTask.state = "run" then m.LoadChannelsTask.control = "stop"
m.LoadChannelsTask.searchTerm = m.top.searchTerm m.LoadChannelsTask.searchTerm = m.top.searchTerm
@ -279,6 +286,7 @@ end sub
function onKeyEvent(key as string, press as boolean) as boolean function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false if not press then return false
detailsGrp = m.top.findNode("detailsPane") detailsGrp = m.top.findNode("detailsPane")
gridGrp = m.top.findNode("scheduleGrid") gridGrp = m.top.findNode("scheduleGrid")

View File

@ -55,14 +55,20 @@ sub itemContentChanged()
' Handle all "As Is" fields ' Handle all "As Is" fields
m.top.overhangTitle = itemData.name m.top.overhangTitle = itemData.name
setFieldText("releaseYear", itemData.productionYear) setFieldText("releaseYear", itemData.productionYear)
setFieldText("officialRating", itemData.officialRating)
setFieldText("overview", itemData.overview) setFieldText("overview", itemData.overview)
if itemData.officialRating <> invalid
setFieldText("officialRating", itemData.officialRating)
else
m.top.findNode("infoGroup").removeChild(m.top.findNode("officialRating"))
end if
if itemData.communityRating <> invalid if itemData.communityRating <> invalid
setFieldText("communityRating", int(itemData.communityRating * 10) / 10) setFieldText("communityRating", int(itemData.communityRating * 10) / 10)
m.top.findNode("star").visible = "true"
else else
' hide the star icon ' hide the star icon
m.top.findNode("communityRatingGroup").visible = false m.top.findNode("infoGroup").removeChild(m.top.findNode("communityRatingGroup"))
end if end if
if itemData.CriticRating <> invalid if itemData.CriticRating <> invalid
@ -86,6 +92,8 @@ sub itemContentChanged()
if itemData.genres.count() > 0 if itemData.genres.count() > 0
setFieldText("genres", tr("Genres") + ": " + itemData.genres.join(", ")) setFieldText("genres", tr("Genres") + ": " + itemData.genres.join(", "))
else
m.top.findNode("details").removeChild(m.top.findNode("genres"))
end if end if
' show tags if there are no genres to display ' show tags if there are no genres to display
@ -101,10 +109,8 @@ sub itemContentChanged()
end for end for
if directors.count() > 0 if directors.count() > 0
setFieldText("director", tr("Director") + ": " + directors.join(", ")) setFieldText("director", tr("Director") + ": " + directors.join(", "))
end if else
m.top.findNode("details").removeChild(m.top.findNode("director"))
if itemData.mediaStreams[0] <> invalid
setFieldText("video_codec", tr("Video") + ": " + itemData.mediaStreams[0].displayTitle)
end if end if
if get_user_setting("ui.details.hidetagline") = "false" if get_user_setting("ui.details.hidetagline") = "false"
@ -115,6 +121,15 @@ sub itemContentChanged()
m.details.removeChild(m.tagline) m.details.removeChild(m.tagline)
end if end if
'set aired date if type is Episode
if itemData.PremiereDate <> invalid and itemData.Type = "Episode"
airDate = CreateObject("roDateTime")
airDate.FromISO8601String(itemData.PremiereDate)
m.top.findNode("aired").text = tr("Aired") + ": " + airDate.AsDateString("short-month-no-weekday")
'remove movie release year label
m.top.findNode("infoGroup").removeChild(m.top.findNode("releaseYear"))
end if
setFavoriteColor() setFavoriteColor()
setWatchedColor() setWatchedColor()
SetUpVideoOptions(itemData.mediaSources) SetUpVideoOptions(itemData.mediaSources)
@ -125,11 +140,28 @@ end sub
sub SetUpVideoOptions(streams) sub SetUpVideoOptions(streams)
videos = [] videos = []
codecDetailsSet = false
for i = 0 to streams.Count() - 1 for i = 0 to streams.Count() - 1
if streams[i].VideoType = "VideoFile" if streams[i].VideoType = "VideoFile"
codec = "" codec = ""
if streams[i].mediaStreams <> invalid and streams[i].mediaStreams.Count() > 0 then codec = streams[i].mediaStreams[0].displayTitle if streams[i].mediaStreams <> invalid and streams[i].mediaStreams.Count() > 0
' find the first (default) video track to get the codec for the details screen
if codecDetailsSet = false
for index = 0 to streams[i].mediaStreams.Count() - 1
if streams[i].mediaStreams[index].Type = "Video"
setFieldText("video_codec", tr("Video") + ": " + streams[i].mediaStreams[index].displayTitle)
codecDetailsSet = true
exit for
end if
end for
end if
codec = streams[i].mediaStreams[0].displayTitle
end if
' Create options for user to switch between video tracks
videos.push({ videos.push({
"Title": streams[i].Name, "Title": streams[i].Name,
"Description": tr("Video"), "Description": tr("Video"),

View File

@ -9,7 +9,7 @@
<Label id="runtime" /> <Label id="runtime" />
<Label id="officialRating" /> <Label id="officialRating" />
<LayoutGroup id="communityRatingGroup" layoutDirection="horiz" itemSpacings="[-5]"> <LayoutGroup id="communityRatingGroup" layoutDirection="horiz" itemSpacings="[-5]">
<Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="32" width="32" blendColor="#cb272a" /> <Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="32" width="32" blendColor="#cb272a" visible="false"/>
<Label id="communityRating" /> <Label id="communityRating" />
</LayoutGroup> </LayoutGroup>
<LayoutGroup layoutDirection="horiz" itemSpacings="[-5]" id="criticRatingGroup"> <LayoutGroup layoutDirection="horiz" itemSpacings="[-5]" id="criticRatingGroup">
@ -17,6 +17,7 @@
<Label id="criticRatingLabel" /> <Label id="criticRatingLabel" />
</LayoutGroup> </LayoutGroup>
<Label id="ends-at" /> <Label id="ends-at" />
<Label id="aired" />
</LayoutGroup> </LayoutGroup>
<Label id="genres" /> <Label id="genres" />
<Label id="director" /> <Label id="director" />

View File

@ -408,7 +408,7 @@ sub onMetaDataLoaded()
if data <> invalid and data.count() > 0 if data <> invalid and data.count() > 0
' Use metadata to load backdrop image ' Use metadata to load backdrop image
if isvalid(data.json) if isValid(data.json)
if isValid(data.json.ArtistItems) if isValid(data.json.ArtistItems)
if data.json.ArtistItems.count() > 0 if data.json.ArtistItems.count() > 0
if isValid(data.json.ArtistItems[0].id) if isValid(data.json.ArtistItems[0].id)

View File

@ -1,21 +1,123 @@
sub init() sub init()
m.top.optionsAvailable = false ' Change once Shuffle option is added m.top.optionsAvailable = true
m.top.overhangVisible = false m.top.overhangVisible = false
m.slideshowTimer = m.top.findNode("slideshowTimer")
m.slideshowTimer.observeField("fire", "nextSlide")
m.status = m.top.findNode("status")
m.textBackground = m.top.findNode("background")
m.statusTimer = m.top.findNode("statusTimer")
m.statusTimer.observeField("fire", "statusUpdate")
m.slideshow = get_user_setting("photos.slideshow")
m.random = get_user_setting("photos.random")
m.showStatusAnimation = m.top.findNode("showStatusAnimation")
m.hideStatusAnimation = m.top.findNode("hideStatusAnimation")
itemContentChanged()
end sub end sub
sub itemContentChanged() sub itemContentChanged()
if isValidToContinue(m.top.itemIndex)
m.LoadLibrariesTask = createObject("roSGNode", "LoadPhotoTask") m.LoadLibrariesTask = createObject("roSGNode", "LoadPhotoTask")
m.LoadLibrariesTask.itemContent = m.top.itemContent itemContent = m.top.items.content.getChild(m.top.itemIndex)
m.LoadLibrariesTask.itemContent = itemContent
m.LoadLibrariesTask.observeField("results", "onPhotoLoaded") m.LoadLibrariesTask.observeField("results", "onPhotoLoaded")
m.LoadLibrariesTask.control = "RUN" m.LoadLibrariesTask.control = "RUN"
end if
end sub end sub
sub onPhotoLoaded() sub onPhotoLoaded()
if m.LoadLibrariesTask.results <> invalid if m.LoadLibrariesTask.results <> invalid
photo = m.top.findNode("photo") photo = m.top.findNode("photo")
photo.uri = m.LoadLibrariesTask.results photo.uri = m.LoadLibrariesTask.results
if m.slideshow = "true" or m.random = "true"
' user has requested either a slideshow or random...
m.slideshowTimer.control = "start"
end if
else else
'Show user error here (for example if it's not a supported image type) 'Show user error here (for example if it's not a supported image type)
message_dialog("This image type is not supported.") message_dialog("This image type is not supported.")
end if end if
end sub end sub
sub nextSlide()
m.slideshowTimer.control = "stop"
if m.slideshow = "true"
if isValidToContinue(m.top.itemIndex + 1)
m.top.itemIndex++
m.slideshowTimer.control = "start"
end if
else if m.random = "true"
index = rnd(m.top.items.content.getChildCount() - 1)
if isValidToContinue(index)
m.top.itemIndex = index
m.slideshowTimer.control = "start"
end if
end if
end sub
sub statusUpdate()
m.statusTimer.control = "stop"
m.hideStatusAnimation.control = "start"
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
if key = "right"
if isValidToContinue(m.top.itemIndex + 1)
m.slideshowTimer.control = "stop"
m.top.itemIndex++
end if
return true
end if
if key = "left"
if isValidToContinue(m.top.itemIndex - 1)
m.slideshowTimer.control = "stop"
m.top.itemIndex--
end if
return true
end if
if key = "play"
if m.slideshowTimer.control = "start"
' stop the slideshow if the user hits "pause"
m.slideshowTimer.control = "stop"
m.status.text = tr("Slideshow Paused")
if m.textBackground.opacity = 0
m.showStatusAnimation.control = "start"
end if
m.statusTimer.control = "start"
else
' start the slideshow if the user hits "play"
m.status.text = tr("Slideshow Resumed")
if m.textBackground.opacity = 0
m.showStatusAnimation.control = "start"
end if
m.slideshow = "true"
m.statusTimer.control = "start"
m.slideshowTimer.control = "start"
end if
return true
end if
if key = "options"
' Options (random etc) is done on itemGrid
return true
end if
return false
end function
function isValidToContinue(index as integer)
if isValid(m.top.items) and isValid(m.top.items.content)
if index >= 0 and index < m.top.items.content.getChildCount()
return true
end if
end if
return false
end function

View File

@ -1,13 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<component name="PhotoDetails" extends="JFGroup"> <component name="PhotoDetails" extends="JFGroup">
<children> <children>
<LayoutGroup id="toplevel">
<Poster id="photo" width="1920" height="1080" loadDisplayMode="scaleToFit"/> <Poster id="photo" width="1920" height="1080" loadDisplayMode="scaleToFit"/>
</LayoutGroup> <Rectangle id="background" color="0x101010EE" height="120" width="500" Translation="[700, -150]" opacity="0">
<Label id="status" font="font:MediumSystemFont" height="100" width="500" horizAlign="center" vertAlign="bottom"/>
</Rectangle>
<Timer id="slideshowTimer" duration="5" repeat="false" />
<Timer id="statusTimer" duration="2" repeat="false" />
<Animation id="showStatusAnimation" duration="1" repeat="false">
<FloatFieldInterpolator key="[0.0, 0.1]" keyValue="[0, 1]" fieldToInterp="background.opacity" />
<Vector2DFieldInterpolator key="[0.1, 1]" keyValue="[[700, -150], [700, -5]]" fieldToInterp="background.translation" />
</Animation>
<Animation id="hideStatusAnimation" duration="1" repeat="false">
<Vector2DFieldInterpolator key="[0.0, 0.9]" keyValue="[[700, -5], [700, -150]]" fieldToInterp="background.translation" />
<FloatFieldInterpolator key="[0.9, 1]" keyValue="[1, 0]" fieldToInterp="background.opacity" />
</Animation>
</children> </children>
<interface> <interface>
<field id="itemContent" type="node" onChange="itemContentChanged" /> <field id="items" type="node" />
<field id="itemIndex" type="integer" value="-1" onChange="itemContentChanged" />
</interface> </interface>
<script type="text/brightscript" uri="PhotoDetails.brs" /> <script type="text/brightscript" uri="PhotoDetails.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" /> <script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component> </component>

View File

@ -1,17 +0,0 @@
sub init()
m.top.functionName = "loadItems"
end sub
sub loadItems()
item = m.top.itemContent
group = CreateObject("roSGNode", "PhotoDetails")
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
group.itemContent = item
' TODO/FIXME:
' Wait some time and move to the next photo...
end sub

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="PhotoPlayerTask" extends="Task">
<interface>
<field id="itemContent" type="node" />
</interface>
<script type="text/brightscript" uri="PhotoPlayerTask.brs" />
</component>

View File

@ -48,7 +48,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
m.searchAlphabox.textEditBox.translation = "[0, 0]" m.searchAlphabox.textEditBox.translation = "[0, 0]"
end if end if
if key = "left" and m.searchSelect.isinFocusChain() and (m.searchSelect.currFocusColumn = -1 or m.searchSelect.currFocusColumn = 0) if key = "left" and m.searchSelect.isinFocusChain()
m.searchAlphabox.setFocus(true) m.searchAlphabox.setFocus(true)
return true return true
else if key = "right" else if key = "right"

View File

@ -18,6 +18,12 @@ sub itemContentChanged()
m.title.text = indexNumber + item.title m.title.text = indexNumber + item.title
m.overview.text = item.overview m.overview.text = item.overview
if itemData.PremiereDate <> invalid
airDate = CreateObject("roDateTime")
airDate.FromISO8601String(itemData.PremiereDate)
m.top.findNode("aired").text = tr("Aired") + ": " + airDate.AsDateString("short-month-no-weekday")
end if
imageUrl = item.posterURL imageUrl = item.posterURL
if get_user_setting("ui.tvshows.blurunwatched") = "true" if get_user_setting("ui.tvshows.blurunwatched") = "true"
@ -30,8 +36,14 @@ sub itemContentChanged()
m.poster.uri = imageUrl m.poster.uri = imageUrl
if type(itemData.RunTimeTicks) = "LongInteger" if type(itemData.RunTimeTicks) = "roInt" or type(itemData.RunTimeTicks) = "LongInteger"
m.top.findNode("runtime").text = stri(getRuntime()).trim() + " mins" runTime = getRuntime()
if runTime < 2
m.top.findNode("runtime").text = "1 min"
else
m.top.findNode("runtime").text = stri(runTime).trim() + " mins"
end if
if get_user_setting("ui.design.hideclock") <> "true" if get_user_setting("ui.design.hideclock") <> "true"
m.top.findNode("endtime").text = tr("Ends at %1").Replace("%1", getEndTime()) m.top.findNode("endtime").text = tr("Ends at %1").Replace("%1", getEndTime())
end if end if
@ -41,7 +53,8 @@ sub itemContentChanged()
m.top.findNode("star").visible = true m.top.findNode("star").visible = true
m.top.findNode("communityRating").text = str(int(itemData.communityRating * 10) / 10) m.top.findNode("communityRating").text = str(int(itemData.communityRating * 10) / 10)
else else
m.top.findNode("star").visible = false
m.top.findnode("infoBar").removeChild(m.top.findnode("rating"))
end if end if
videoIdx = invalid videoIdx = invalid

View File

@ -8,13 +8,14 @@
<!-- Using poster of 1 length to get spacing. Not successful with adding translation to title --> <!-- Using poster of 1 length to get spacing. Not successful with adding translation to title -->
<Poster id="null" height="1" /> <Poster id="null" height="1" />
<ScrollingLabel id="title" font="font:MediumBoldSystemFont" maxWidth="950" /> <ScrollingLabel id="title" font="font:MediumBoldSystemFont" maxWidth="950" />
<LayoutGroup layoutDirection="horiz" itemSpacings="[20]"> <LayoutGroup id="infoBar" layoutDirection="horiz" itemSpacings="[20]">
<Label id="runtime" font="font:SmallestSystemFont" /> <Label id="runtime" font="font:SmallestSystemFont" />
<LayoutGroup layoutDirection="horiz" itemSpacings="[-5]"> <LayoutGroup id="rating" layoutDirection="horiz" itemSpacings="[-5]">
<Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="26" width="26" blendColor="#cb272a" /> <Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="26" width="26" blendColor="#cb272a" />
<Label id="communityRating" font="font:SmallestSystemFont" /> <Label id="communityRating" font="font:SmallestSystemFont" />
</LayoutGroup> </LayoutGroup>
<Label id="endtime" font="font:SmallestSystemFont" /> <Label id="endtime" font="font:SmallestSystemFont" />
<Label id="aired" font="font:SmallestSystemFont" />
</LayoutGroup> </LayoutGroup>
<Label id="overview" font="font:SmallestSystemFont" wrap="true" height="130" width="950" maxLines="3" ellipsizeOnBoundary="true"/> <Label id="overview" font="font:SmallestSystemFont" wrap="true" height="130" width="950" maxLines="3" ellipsizeOnBoundary="true"/>
<LayoutGroup layoutDirection="horiz" itemSpacings="[15]"> <LayoutGroup layoutDirection="horiz" itemSpacings="[15]">

View File

@ -17,14 +17,31 @@ sub itemContentChanged()
' Handle all "As Is" fields ' Handle all "As Is" fields
m.top.overhangTitle = itemData.name m.top.overhangTitle = itemData.name
'Check production year, if invalid remove label
if itemData.productionYear <> invalid
setFieldText("releaseYear", 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
setFieldText("officialRating", 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 itemData.communityRating <> invalid
m.top.findNode("star").visible = true m.top.findNode("star").visible = true
setFieldText("communityRating", int(itemData.communityRating * 10) / 10) setFieldText("communityRating", int(itemData.communityRating * 10) / 10)
else else
m.top.findNode("main_group").removeChild(m.top.findNode("communityRating"))
m.top.findNode("main_group").removeChild(m.top.findNode("star"))
m.top.findNode("star").visible = false m.top.findNode("star").visible = false
end if end if
setFieldText("overview", itemData.overview) setFieldText("overview", itemData.overview)
@ -32,11 +49,17 @@ sub itemContentChanged()
setFieldText("runtime", stri(getRuntime()) + " mins") setFieldText("runtime", stri(getRuntime()) + " mins")
end if end if
'History feild is set via the function getHistory()
setFieldText("history", getHistory()) setFieldText("history", getHistory())
'Check genres, if invalid remove label
if itemData.genres.count() > 0 if itemData.genres.count() > 0
setFieldText("genres", itemData.genres.join(", ")) setFieldText("genres", itemData.genres.join(", "))
else
m.top.findNode("main_group").removeChild(m.top.findNode("genres"))
end if end if
'We don't display Directors in the show page. Might want to remove this.
for each person in itemData.people for each person in itemData.people
if person.type = "Director" if person.type = "Director"
exit for exit for
@ -44,6 +67,8 @@ sub itemContentChanged()
end for end for
if itemData.taglines.count() > 0 if itemData.taglines.count() > 0
setFieldText("tagline", itemData.taglines[0]) setFieldText("tagline", itemData.taglines[0])
else
m.top.findNode("main_group").removeChild(m.top.findNode("tagline"))
end if end if
end sub end sub
@ -105,6 +130,7 @@ function getHistory() as string
end if end if
if studio = invalid and airwords = invalid if studio = invalid and airwords = invalid
m.top.findNode("main_group").removeChild(m.top.findNode("history"))
return "" return ""
end if end if

View File

@ -9,7 +9,7 @@
<Label id="releaseYear" /> <Label id="releaseYear" />
<Label id="officialRating" /> <Label id="officialRating" />
<LayoutGroup layoutDirection="horiz" itemSpacings="[3]"> <LayoutGroup layoutDirection="horiz" itemSpacings="[3]">
<Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="32" width="32" blendColor="#cb272a" /> <Poster id="star" uri="pkg:/images/sharp_star_white_18dp.png" height="32" width="32" blendColor="#cb272a" visible="false" />
<Label id="communityRating" /> <Label id="communityRating" />
</LayoutGroup> </LayoutGroup>
</LayoutGroup> </LayoutGroup>

View File

@ -8295,5 +8295,134 @@
<source>Change Server</source> <source>Change Server</source>
<translation>Server wechseln</translation> <translation>Server wechseln</translation>
</message> </message>
<message>
<source>Save Credentials?</source>
<translation>Zugangsdaten speichern?</translation>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Fehler beim Abrufen des Inhaltes</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>Error During Playback</source>
<translation>Fehler bei der Wiedergabe</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Es ist ein Fehler beim Abrufen der Daten vom Server aufgetreten.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Bei der Wiedergabe dieses Elements ist ein Fehler aufgetreten.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Unable to load Channel Data from the server</source>
<translation>Kanaldaten können nicht vom Server geladen werden</translation>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Dieser %1 enthält keine Dateien</translation>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Name</translation>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Kritikerbewertung</translation>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Veröffentlichungsdatum</translation>
</message>
<message>
<comment>Title of Tab for options to sort library content</comment>
<source>TAB_SORT</source>
<translation>Sortierung</translation>
</message>
<message>
<source>Born</source>
<translation>Geboren am</translation>
</message>
<message>
<source>Age</source>
<translation>Alter</translation>
</message>
<message>
<source>Cast &amp; Crew</source>
<translation>Besetzung &amp; Mitwirkende</translation>
</message>
<message>
<source>Died</source>
<translation>Gestorben am</translation>
</message>
<message>
<source>Sign Out</source>
<translation>Abmelden</translation>
</message>
<message>
<source>On Now</source>
<translation>Jetzt verfügbar</translation>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Fehler beim Laden der Kanalinformationen</translation>
</message>
<message>
<comment>Title of Tab for switching &quot;views&quot; when looking at a library</comment>
<source>TAB_VIEW</source>
<translation>Übersicht</translation>
</message>
<message>
<source>More Like This</source>
<translation>Ähnliches</translation>
</message>
<message>
<source>Movies</source>
<translation>Filme</translation>
</message>
<message>
<source>RUNTIME</source>
<translation>Laufzeit</translation>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Altersfreigabe</translation>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Lade Daten des Kanals</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>IMDb Bewertung</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Wiedergabezähler</translation>
</message>
<message>
<comment>Title of Tab for options to filter library content</comment>
<source>TAB_FILTER</source>
<translation>Filter</translation>
</message>
<message>
<source>Change Server</source>
<translation>Server wechseln</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Abgespielt am</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Hinzugefügt am</translation>
</message>
</context> </context>
</TS> </TS>

View File

@ -1844,5 +1844,276 @@
<source>Disabled</source> <source>Disabled</source>
<translation>Disabled</translation> <translation>Disabled</translation>
</message> </message>
<message>
<source>Use voice remote to search</source>
<translation>Use voice remote to search</translation>
<extracomment>Help text in search voice text box</extracomment>
</message>
<message>
<source>Use the replay button to slowly animate to the first item in the folder. (If disabled, the folder will reset to the first item immediately).</source>
<translation>Use the replay button to slowly animate to the first item in the folder. (If disabled, the folder will reset to the first item immediately).</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Options for TV Shows.</source>
<translation>Options for TV Shows.</translation>
<extracomment>Description for TV Shows user settings.</extracomment>
</message>
<message>
<source>Cinema Mode brings the theater experience straight to your living room with the ability to play custom intros before the main feature.</source>
<translation>Cinema Mode brings the cinema experience straight to your living room with the ability to play custom intros before the main feature.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Settings relating to playback and supported codec and media types.</source>
<translation>Settings relating to playback and supported codec and media types.</translation>
</message>
<message>
<source>Settings relating to how the application looks.</source>
<translation>Settings relating to how the application looks.</translation>
</message>
<message>
<source>Studios</source>
<translation>Studios</translation>
</message>
<message>
<source>Return to Top</source>
<translation>Return to Top</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Title in user setting screen.</extracomment>
</message>
<message>
<source>Details Page</source>
<translation>Details Page</translation>
</message>
<message>
<source>Hide Taglines</source>
<translation>Hide Taglines</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Use Splashscreen as Screensaver Background</source>
<translation>Use Splashscreen as Screensaver Background</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Playback Information</source>
<translation>Playback Information</translation>
</message>
<message>
<source>Transcoding Information</source>
<translation>Transcoding Information</translation>
</message>
<message>
<source>Video Codec</source>
<translation>Video Codec</translation>
</message>
<message>
<source>Audio Codec</source>
<translation>Audio Codec</translation>
</message>
<message>
<source>direct</source>
<translation>direct</translation>
</message>
<message>
<source>Level</source>
<translation>Level</translation>
<extracomment>Video profile level</extracomment>
</message>
<message>
<source>Bit Rate</source>
<translation>Bit Rate</translation>
<extracomment>Video streaming bit rate</extracomment>
</message>
<message>
<source>Size</source>
<translation>Size</translation>
<extracomment>Video size</extracomment>
</message>
<message>
<source>WxH</source>
<translation>WxH</translation>
<extracomment>Video width x height</extracomment>
</message>
<message>
<source>Quick Connect</source>
<translation>Quick Connect</translation>
</message>
<message>
<source>Here is your Quick Connect code:</source>
<translation>Here is your Quick Connect code:</translation>
</message>
<message>
<source>(Dialog will close automatically)</source>
<translation>(Dialogue will close automatically)</translation>
</message>
<message>
<source>Play Trailer</source>
<translation>Play Trailer</translation>
</message>
<message>
<source>Home Page</source>
<translation>Home Page</translation>
</message>
<message>
<source>Options for Home Page.</source>
<translation>Options for Home Page.</translation>
<extracomment>Description for Home Page user settings.</extracomment>
</message>
<message>
<source>Cinema Mode</source>
<translation>Cinema Mode</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Use Splashscreen as Home Background</source>
<translation>Use Splashscreen as Home Background</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Design Elements</source>
<translation>Design Elements</translation>
</message>
<message>
<source>Screensaver</source>
<translation>Screensaver</translation>
</message>
<message>
<source>Search now</source>
<translation>Search now</translation>
<extracomment>Help text in search Box</extracomment>
</message>
<message>
<source>Go to episode</source>
<translation>Go to episode</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Episode Detail Page</extracomment>
</message>
<message>
<source>Go to season</source>
<translation>Go to season</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Season Page</extracomment>
</message>
<message>
<source>You can search for Titles, People, Live TV Channels and more</source>
<translation>You can search for Titles, People, Live TV Channels and more</translation>
<extracomment>Help text in search results</extracomment>
</message>
<message>
<source>%1 of %2</source>
<translation>%1 of %2</translation>
<extracomment>Item position and count. %1 = current item. %2 = total number of items</extracomment>
</message>
<message>
<source>There was an error authenticating via Quick Connect.</source>
<translation>There was an error authenticating via Quick Connect.</translation>
</message>
<message>
<source>Networks</source>
<translation>Networks</translation>
</message>
<message>
<source>Shows</source>
<translation>Shows</translation>
</message>
<message>
<source>Options for Details pages.</source>
<translation>Options for Details pages.</translation>
<extracomment>Description for Details page user settings.</extracomment>
</message>
<message>
<source>Hides tagline text on details pages.</source>
<translation>Hides tagline text on details pages.</translation>
</message>
<message>
<source>Blur Unwatched Episodes</source>
<translation>Blur Unwatched Episodes</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>If enabled, images of unwatched episodes will be blurred.</source>
<translation>If enabled, images of unwatched episodes will be blurred.</translation>
</message>
<message>
<source>Options for Jellyfin&apos;s screensaver.</source>
<translation>Options for Jellyfin&apos;s screensaver.</translation>
<extracomment>Description for Screensaver user settings.</extracomment>
</message>
<message>
<source>Use generated splashscreen image as Jellyfin&apos;s screensaver background. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Use generated splashscreen image as Jellyfin&apos;s screensaver background. Jellyfin will need to be closed and reopened for change to take effect.</translation>
</message>
<message>
<source>Options that alter the design of Jellyfin.</source>
<translation>Options that alter the design of Jellyfin.</translation>
<extracomment>Description for Design Elements user settings.</extracomment>
</message>
<message>
<source>Use generated splashscreen image as Jellyfin&apos;s home background. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Use generated splashscreen image as Jellyfin&apos;s home background. Jellyfin will need to be closed and reopened for change to take effect.</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Hide Clock</source>
<translation>Hide Clock</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Max Days Next Up</source>
<translation>Max Days Next Up</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Set the maximum amount of days a show should stay in the &apos;Next Up&apos; list without watching it.</source>
<translation>Set the maximum amount of days a show should stay in the &apos;Next Up&apos; list without watching it.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Reason</source>
<translation>Reason</translation>
</message>
<message>
<source>Total Bitrate</source>
<translation>Total Bitrate</translation>
</message>
<message>
<source>Audio Channels</source>
<translation>Audio Channels</translation>
</message>
<message>
<source>Stream Information</source>
<translation>Stream Information</translation>
</message>
<message>
<source>Codec</source>
<translation>Codec</translation>
</message>
<message>
<source>Codec Tag</source>
<translation>Codec Tag</translation>
</message>
<message>
<source>Container</source>
<translation>Container</translation>
<extracomment>Video streaming container</extracomment>
</message>
<message>
<source>Video range type</source>
<translation>Video range type</translation>
</message>
<message>
<source>Pixel format</source>
<translation>Pixel format</translation>
<extracomment>Video pixel format</extracomment>
</message>
<message>
<source>Unable to find any albums or songs belonging to this artist</source>
<translation>Unable to find any albums or songs belonging to this artist</translation>
<extracomment>Popup message when we find no audio data for an artist</extracomment>
</message>
</context> </context>
</TS> </TS>

View File

@ -285,13 +285,18 @@
<source>More Like This</source> <source>More Like This</source>
<translation>More Like This</translation> <translation>More Like This</translation>
</message> </message>
<message>
<source>Special Features</source>
<translation>Special Features</translation>
<message> <message>
<source>Press &apos;OK&apos; to Close</source> <source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</translation> <translation>Press &apos;OK&apos; to Close</translation>
</message> </message>
<message>
<source>Special Features</source>
<translation>Special Features</translation>
</message>
<message>
<source>Additional Parts</source>
<translation>Additional Parts</translation>
<extracomment>Additional parts of a video</extracomment>
</message> </message>
<message> <message>
<source>Movies</source> <source>Movies</source>
@ -489,6 +494,16 @@
<translation>Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.</translation> <translation>Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.</translation>
<extracomment>Settings Menu - Description for option</extracomment> <extracomment>Settings Menu - Description for option</extracomment>
</message> </message>
<message>
<source>AV1 Support</source>
<translation>AV1 Support</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>** EXPERIMENTAL** Support Direct Play of AV1 content if this Roku device supports it.</source>
<translation>** EXPERIMENTAL** Support Direct Play of AV1 content if this Roku device supports it.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message> <message>
<source>Enabled</source> <source>Enabled</source>
<translation>Enabled</translation> <translation>Enabled</translation>
@ -703,10 +718,34 @@
<translation>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</translation> <translation>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</translation>
<extracomment>Settings Menu - Description for option</extracomment> <extracomment>Settings Menu - Description for option</extracomment>
</message> </message>
<message>
<source>Next episode</source>
<translation>Next episode</translation>
</message>
<message> <message>
<source>Play Trailer</source> <source>Play Trailer</source>
<translation>Play Trailer</translation> <translation>Play Trailer</translation>
</message> </message>
<message>
<source>Direct Play H.264 Unsupported Profile Levels</source>
<translation>Direct Play H.264 Unsupported Profile Levels</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Attempt Direct Play for H.264 media with unsupported profile levels before falling back to transcoding if it fails.</source>
<translation>Attempt Direct Play for H.264 media with unsupported profile levels before falling back to transcoding if it fails.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Direct Play HEVC Unsupported Profile Levels</source>
<translation>Direct Play HEVC Unsupported Profile Levels</translation>
<extracomment>Settings Menu - Title for option</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>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message> <message>
<source>Settings relating to playback and supported codec and media types.</source> <source>Settings relating to playback and supported codec and media types.</source>
<translation>Settings relating to playback and supported codec and media types.</translation> <translation>Settings relating to playback and supported codec and media types.</translation>
@ -817,5 +856,49 @@
<translation>Unable to find any albums or songs belonging to this artist</translation> <translation>Unable to find any albums or songs belonging to this artist</translation>
<extracomment>Popup message when we find no audio data for an artist</extracomment> <extracomment>Popup message when we find no audio data for an artist</extracomment>
</message> </message>
<message>
<source>all</source>
<translation>all</translation>
<extracomment>all will reset the searchTerm so all data will be availible</extracomment>
</message>
<message>
<source>Aired</source>
<translation>Aired</translation>
<extracomment>Aired date label</extracomment>
</message>
<message>
<source>Slideshow Off</source>
<translation>Slideshow Off</translation>
</message>
<message>
<source>Slideshow On</source>
<translation>Slideshow On</translation>
</message>
<message>
<source>Slideshow Paused</source>
<translation>Slideshow Paused</translation>
</message>
<message>
<source>Slideshow Resumed</source>
<translation>Slideshow Resumed</translation>
</message>
<message>
<source>Random Off</source>
<translation>Random Off</translation>
</message>
<message>
<source>Random On</source>
<translation>Random On</translation>
</message>
<message>
<source>MPEG-4 Support</source>
<translation>MPEG-4 Support</translation>
<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>
</message>
</context> </context>
</TS> </TS>

View File

@ -1614,5 +1614,104 @@
<source>Save Credentials?</source> <source>Save Credentials?</source>
<translation>Guardar credenciales?</translation> <translation>Guardar credenciales?</translation>
</message> </message>
<message>
<source>Save Credentials?</source>
<translation>Guardar Credenciales?</translation>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Error al recuperar contenido</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Se ha encontrado un error mientras se reproducía este ítem.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Error al cargar datos del canal</translation>
</message>
<message>
<source>Movies</source>
<translation>Peliculas</translation>
</message>
<message>
<source>TV Shows</source>
<translation>Programa de TV</translation>
</message>
<message>
<source>today</source>
<translation>Hoy</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>yesterday</source>
<translation>Ayer</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>Error During Playback</source>
<translation>Error durante la reproducción</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Nombre</translation>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Calificación de los Críticos</translation>
</message>
<message>
<source>Monday</source>
<translation>Lunes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Cargando información del canal</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Borrado confirmado</translation>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Ocurrió un error al recuperar los datos para este ítem desde el servidor.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<source>Unable to load Channel Data from the server</source>
<translation>No se pudo cargar los datos del canal desde el servidor</translation>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Este %1 no contiene ítems</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>Calificación IMDb</translation>
</message>
<message>
<source>Special Features</source>
<translation>Funciones especiales</translation>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</translation>
</message>
</message>
<message>
<source>tomorrow</source>
<translation>Mañana</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Sunday</source>
<translation>Domingo</translation>
<extracomment>Day of Week</extracomment>
</message>
</context> </context>
</TS> </TS>

View File

@ -1884,5 +1884,320 @@
<source>TITLE</source> <source>TITLE</source>
<translation>Nombre</translation> <translation>Nombre</translation>
</message> </message>
<message>
<source>Error loading Channel Data</source>
<translation>Error de Reproducción de Contenido de Canal</translation>
</message>
<message>
<source>Sign Out</source>
<translation>Terminar Sesión</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Borrar Credenciales</translation>
</message>
<message>
<source>Age</source>
<translation>Edad</translation>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Criticas Raiting</translation>
</message>
<message>
<source>Born</source>
<translation>Nacido/a</translation>
</message>
<message>
<source>today</source>
<translation>hoy</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Reproduciendo Contenido de Canal</translation>
</message>
<message>
<source>Channels</source>
<translation>Canales</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>Died</source>
<translation>Muerto/a</translation>
</message>
<message>
<source>View Channel</source>
<translation>Ver Canales</translation>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>si no hay servidores disponibles, puedes agregar manualmente la URL</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Ha ocurrido un error tratando de recuperar la información desde el servidor.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Este %1 no contiene elementos</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>IMDb Raiting</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Fecha Agregada</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Fecha Reproducida</translation>
</message>
<message>
<source>Movies</source>
<translation>Películas</translation>
</message>
<message>
<source>yesterday</source>
<translation>ayer</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>tomorrow</source>
<translation>mañana</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Tuesday</source>
<translation>Martes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>Jueves</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Viernes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<comment>Title of Tab for options to filter library content</comment>
<source>TAB_FILTER</source>
<translation>Filtro</translation>
</message>
<message>
<source>Sunday</source>
<translation>Domingo</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>Lunes</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>Miercoles</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Sabado</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Comienza a</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>Live</source>
<translation>En Vivo</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Repetir</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>TV Guide</source>
<translation>Guia de Television</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
<message>
<source>Record</source>
<translation>Grabar</translation>
</message>
<message>
<source>Record Series</source>
<translation>Grabar Series</translation>
</message>
<message>
<source>Close</source>
<translation>Cerrar</translation>
</message>
<message>
<source>Unknown</source>
<translation>desconocido</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>Connecting to Server</source>
<translation>Conectando con el Servidor</translation>
<extracomment>Message to display to user while client is attempting to connect to the server</extracomment>
</message>
<message>
<source>Not found</source>
<translation>No se ha encontrado</translation>
<extracomment>Title of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Save Credentials?</source>
<translation>Guardar Credenciales?</translation>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Nombre</translation>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Ha ocurrido un error al reproducir este contenido.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Error Recuperando Contenido</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Padres Raiting</translation>
</message>
<message>
<source>Change Server</source>
<translation>Cambiar de Servidor</translation>
</message>
<message>
<comment>Title of Tab for options to sort library content</comment>
<source>TAB_SORT</source>
<translation>Clasificar</translation>
</message>
<message>
<comment>Title of Tab for switching &quot;views&quot; when looking at a library</comment>
<source>TAB_VIEW</source>
<translation>Vista</translation>
</message>
<message>
<source>RUNTIME</source>
<translation>Tiempo de Ejecución</translation>
</message>
<message>
<source>Error During Playback</source>
<translation>Error Durante la Reproducción</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>Unable to load Channel Data from the server</source>
<translation>No se ha podido reproducir el Contenido del Canal de este servidor</translation>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Fecha de Premiere</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Cuenta de Reproducción</translation>
</message>
<message>
<source>More Like This</source>
<translation>Mas de este Estilo</translation>
</message>
<message>
<source>Special Features</source>
<translation>Funciones Especiales</translation>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</translation>
</message>
</message>
<message>
<source>TV Shows</source>
<translation>Programas de Televisión</translation>
</message>
<message>
<source>Ends at</source>
<translation>Termina a</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Cancel Recording</source>
<translation>Cancelar la grabación</translation>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Cancelar la grabación de Series</translation>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Elige un servidor Jellyfin disponible de la red local</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>Started</source>
<translation>Comenzó</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Comenzó a</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Starts</source>
<translation>Comienza</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ended at</source>
<translation>Terminó</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Agregar el nombre del servidor o direccion de IP</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Elige un servidor Jellyfin disponible de la red local:</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>si no hay servidores disponibles, puedes agregar manualmente la URL:</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Ha ocurrido un error tratando de recuperar la información desde el servidor.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Reproduciendo Contenido de Canal</translation>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Este %1 no contiene elementos</translation>
</message>
</context> </context>
</TS> </TS>

View File

@ -3605,5 +3605,441 @@
<source>RELEASE_DATE</source> <source>RELEASE_DATE</source>
<translation>Date de sortie</translation> <translation>Date de sortie</translation>
</message> </message>
<message>
<source>Return to Top</source>
<translation>Retour en haut</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Title in user setting screen.</extracomment>
</message>
<message>
<source>Here is your Quick Connect code:</source>
<translation>Voici votre code Quick Connect :</translation>
</message>
<message>
<source>Error Getting Playback Information</source>
<translation>Erreur lors de l&apos;obtention des informations de lecture</translation>
<extracomment>Dialog Title: Received error from server when trying to get information about the selected item for playback</extracomment>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Sélectionnez un serveur Jellyfin disponible sur votre réseau local :</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>Cancel Recording</source>
<translation>Annuler l&apos;enregistrement</translation>
</message>
<message>
<source>Record</source>
<translation>Enregistrer</translation>
</message>
<message>
<source>Screensaver</source>
<translation>Écran de veille</translation>
</message>
<message>
<source>Go to episode</source>
<translation>Aller à l&apos;épisode</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Episode Detail Page</extracomment>
</message>
<message>
<source>Go to series</source>
<translation>Aller à la série</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Series Detail Page</extracomment>
</message>
<message>
<source>Studios</source>
<translation>Studios</translation>
</message>
<message>
<source>Search now</source>
<translation>Rechercher maintenant</translation>
<extracomment>Help text in search Box</extracomment>
</message>
<message>
<source>You can search for Titles, People, Live TV Channels and more</source>
<translation>Vous pouvez rechercher des titres, des personnes, des chaînes de télévision en direct et plus encore</translation>
<extracomment>Help text in search results</extracomment>
</message>
<message>
<source>Quick Connect</source>
<translation>Quick Connect</translation>
</message>
<message>
<source>%1 of %2</source>
<translation>%1 sur %2</translation>
<extracomment>Item position and count. %1 = current item. %2 = total number of items</extracomment>
</message>
<message>
<source>Options for Jellyfin&apos;s screensaver.</source>
<translation>Options de l&apos;écran de veille de Jellyfin.</translation>
<extracomment>Description for Screensaver user settings.</extracomment>
</message>
<message>
<source>Cinema Mode</source>
<translation>Mode Cinéma</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Cinema Mode brings the theater experience straight to your living room with the ability to play custom intros before the main feature.</source>
<translation>Le Mode Cinéma vous fait vivre l&apos;expérience du cinéma directement dans votre salon en vous permettant de diffuser des intros personnalisées avant le film.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>WxH</source>
<translation>WxH</translation>
<extracomment>Video width x height</extracomment>
</message>
<message>
<source>Shows</source>
<translation>Séries</translation>
</message>
<message>
<source>Networks</source>
<translation>Réseaux</translation>
</message>
<message>
<source>User Interface</source>
<translation>Interface Utilisateur</translation>
<extracomment>Title for User Interface section in user setting screen.</extracomment>
</message>
<message>
<source>Close</source>
<translation>Fermer</translation>
</message>
<message>
<source>Play Trailer</source>
<translation>Lire la bande-annonce</translation>
</message>
<message>
<source>Item Count</source>
<translation>Nombre d&apos;éléments</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Count in user setting screen.</extracomment>
</message>
<message>
<source>Version</source>
<translation>Version</translation>
</message>
<message>
<source>Connecting to Server</source>
<translation>Connexion au Serveur</translation>
<extracomment>Message to display to user while client is attempting to connect to the server</extracomment>
</message>
<message>
<source>TV Guide</source>
<translation>Guide Télé</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
<message>
<source>Not found</source>
<translation>Introuvable</translation>
<extracomment>Title of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>The requested content does not exist on the server</source>
<translation>Le contenu demandé n&apos;existe pas sur le serveur</translation>
<extracomment>Content of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Entrer le nom du serveur ou son adresse IP</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>Go to season</source>
<translation>Aller à la saison</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Season Page</extracomment>
</message>
<message>
<source>Options for TV Shows.</source>
<translation>Options pour les Séries Télé.</translation>
<extracomment>Description for TV Shows user settings.</extracomment>
</message>
<message>
<source>Blur Unwatched Episodes</source>
<translation>Flouter les épisodes non visionnés</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>If enabled, images of unwatched episodes will be blurred.</source>
<translation>Si activé, les images des épisodes non visionnés seront floutées.</translation>
</message>
<message>
<source>Video Codec</source>
<translation>Codec Vidéo</translation>
</message>
<message>
<source>Audio Codec</source>
<translation>Codec Audio</translation>
</message>
<message>
<source>Audio Channels</source>
<translation>Canaux Audio</translation>
</message>
<message>
<source>Codec</source>
<translation>Codec</translation>
</message>
<message>
<source>Codec Tag</source>
<translation>Balise de Codec</translation>
</message>
<message>
<source>Container</source>
<translation>Conteneur</translation>
<extracomment>Video streaming container</extracomment>
</message>
<message>
<source>Size</source>
<translation>Taille</translation>
<extracomment>Video size</extracomment>
</message>
<message>
<source>Unknown</source>
<translation>Inconnu</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>Enabled</source>
<translation>Activé</translation>
</message>
<message>
<source>Disabled</source>
<translation>Désactivé</translation>
</message>
<message>
<source>Playback</source>
<translation>Lecture</translation>
<extracomment>Title for Playback section in user setting screen.</extracomment>
</message>
<message>
<source>An error was encountered while playing this item. Server did not provide required transcoding data.</source>
<translation>Une erreur a é rencontrée lors de la lecture de ce fichier. Le serveur n&apos;a pas communiqué les données nécessaires pour le transcodage.</translation>
<extracomment>Content of message box when trying to play an item which requires transcoding, and the server did not provide transcode url</extracomment>
</message>
<message>
<source>Always show the titles below the poster images. (If disabled, the title will be shown under the highlighted item only).</source>
<translation>Toujours afficher les titres sous les images des affiches. (Si désactivé, les titres s&apos;afficherons uniquement sous les éléments en surbrillance).</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Show item count in the library and index of selected item.</source>
<translation>Afficher le nombre d&apos;éléments dans la bibliotèque et l&apos;index de l&apos;élément sélectionné.</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Use voice remote to search</source>
<translation>Utiliser la télécomande vocale pour rechercher</translation>
<extracomment>Help text in search voice text box</extracomment>
</message>
<message>
<source>(Dialog will close automatically)</source>
<translation>(La boîte de dialogue se fermera automatiquement)</translation>
</message>
<message>
<source>Use the replay button to slowly animate to the first item in the folder. (If disabled, the folder will reset to the first item immediately).</source>
<translation>Utiliser la touche Replay pour lentement animer la sélection du première élément du dossier. (Si désactivé, le premier élément du dossier sera immédiatement sélectionné).</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Options for Details pages.</source>
<translation>Options des Pages de Détails.</translation>
<extracomment>Description for Details page user settings.</extracomment>
</message>
<message>
<source>Options for TV Shows.</source>
<translation>Options pour les Séries Télévisée.</translation>
<extracomment>Description for TV Shows user settings.</extracomment>
</message>
<message>
<source>View Channel</source>
<translation>Voir la Chaîne</translation>
</message>
<message>
<source>Use Splashscreen as Screensaver Background</source>
<translation>Utiliser le Splash Screen comme fond d&apos;écran de veille</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>WxH</source>
<translation>LxH</translation>
<extracomment>Video width x height</extracomment>
</message>
<message>
<source>Use generated splashscreen image as Jellyfin&apos;s screensaver background. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Utiliser le Splash Screen généré comme fond d&apos;écran de veille de Jellyfin. Jellyfin devra être fermé et rouvert pour que le changement prenne effet.</translation>
</message>
<message>
<source>Use generated splashscreen image as Jellyfin&apos;s home background. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Utiliser le Splash Screen généré comme arrière plan principal de Jellyfin. Jellyfin devra être fermé et rouvert pour que le changement prenne effet.</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</message>
<message>
<source>Hide Clock</source>
<translation>Masquer l&apos;Horloge</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</source>
<translation>Masquer toutes les horloges dans Jellyfin. Jellyfin devra être fermé et rouvert pour que le changement prenne effet.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Settings relating to how the application looks.</source>
<translation>Réglages relatifs à l&apos;apparence de l&apos;application.</translation>
</message>
<message>
<source>Pixel format</source>
<translation>Format des pixels</translation>
<extracomment>Video pixel format</extracomment>
</message>
<message>
<source>Unable to find any albums or songs belonging to this artist</source>
<translation>Aucuns n&apos;albums ni chansons ont é trouvés pour cet artiste</translation>
<extracomment>Popup message when we find no audio data for an artist</extracomment>
</message>
<message>
<source>Starts</source>
<translation>Débute</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Record Series</source>
<translation>Enregistrer la Série</translation>
</message>
<message>
<source>Hide Taglines</source>
<translation>Masquer les étiquettes</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Media Grid options.</source>
<translation>Options de la Grille Média.</translation>
</message>
<message>
<source>Set Favorite</source>
<translation>Mettre en Favori</translation>
<extracomment>Button Text - When pressed, sets item as Favorite</extracomment>
</message>
<message>
<source>Details Page</source>
<translation>Page de Détails</translation>
</message>
<message>
<source>Options for Home Page.</source>
<translation>Options pour la Page d&apos;Accueil.</translation>
<extracomment>Description for Home Page user settings.</extracomment>
</message>
<message>
<source>Max Days Next Up</source>
<translation>Nombre Max de jours dans Suivant</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Set the maximum amount of days a show should stay in the &apos;Next Up&apos; list without watching it.</source>
<translation>Définir le nombre maximum de jours une émission doit rester dans la liste &quot;Suivant&quot; sans la regarder.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Playback Information</source>
<translation>Informations de Lecture</translation>
</message>
<message>
<source>Design Elements</source>
<translation>Élements de Désign</translation>
</message>
<message>
<source>Home Page</source>
<translation>Page d&apos;Accueil</translation>
</message>
<message>
<source>Transcoding Information</source>
<translation>Informations de Transcodage</translation>
</message>
<message>
<source>Options that alter the design of Jellyfin.</source>
<translation>Options qui modifient le design de Jellyfin.</translation>
<extracomment>Description for Design Elements user settings.</extracomment>
</message>
<message>
<source>Use Splashscreen as Home Background</source>
<translation>Utiliser le Splash Screen comme arrière-plan principal</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Media Grid</source>
<translation>Grille Média</translation>
<extracomment>UI -&gt; Media Grid section in user setting screen.</extracomment>
</message>
<message>
<source>MPEG-2 Support</source>
<translation>Support MPEG-2</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Annuler l&apos;enregistrement de la Série</translation>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>Si aucun serveur n&apos;est affiché ci-dessus vous pouvez également saisir l&apos;adresse IP du serveur manuellement :</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.</source>
<translation>Prise en charge de la lecture directe du codec MPEG-2 (ex., Live TV). Cela empêchera le transcodage du contenu MPEG-2 mais utilisera sensiblement plus de bande passante.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Item Titles</source>
<translation>Titres des éléments</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Title in user setting screen.</extracomment>
</message>
<message>
<source>Set Watched</source>
<translation>Mettre en Visionné</translation>
<extracomment>Button Text - When pressed, marks item as Warched</extracomment>
</message>
<message>
<source>There was an error authenticating via Quick Connect.</source>
<translation>Une erreur s&apos;est produite lors de l&apos;autentification via Quick Connect.</translation>
</message>
<message>
<source>Hides tagline text on details pages.</source>
<translation>Masquer les étiquettes sur la page des détails.</translation>
</message>
<message>
<source>Settings relating to playback and supported codec and media types.</source>
<translation>Réglages relatifs à la lecture, aux codecs pris en charges et aux types de médias.</translation>
</message>
<message>
<source>Reason</source>
<translation>Raison</translation>
</message>
<message>
<source>direct</source>
<translation>direct</translation>
</message>
<message>
<source>Total Bitrate</source>
<translation>Débit Total</translation>
</message>
<message>
<source>Stream Information</source>
<translation>Informations du Flux</translation>
</message>
<message>
<source>Level</source>
<translation>Niveau</translation>
<extracomment>Video profile level</extracomment>
</message>
<message>
<source>Bit Rate</source>
<translation>Débit</translation>
<extracomment>Video streaming bit rate</extracomment>
</message>
<message>
<source>Video range type</source>
<translation>Différents types de vidéo</translation>
</message>
</context> </context>
</TS> </TS>

View File

@ -7642,5 +7642,17 @@ elemeket</translation>
<source>Save Credentials?</source> <source>Save Credentials?</source>
<translation>Mented a hitelesítő adatokat?</translation> <translation>Mented a hitelesítő adatokat?</translation>
</message> </message>
<message>
<source>Delete Saved</source>
<translation>Mentettek Törlése</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Mented a hitelesítő adatokat?</translation>
</message>
<message>
<source>On Now</source>
<translation>Most</translation>
</message>
</context> </context>
</TS> </TS>

View File

@ -1723,5 +1723,203 @@
<translation>È stato riscontrato un errore durante la riproduzione di questo oggetto.</translation> <translation>È stato riscontrato un errore durante la riproduzione di questo oggetto.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment> <extracomment>Dialog detail when error occurs during playback</extracomment>
</message> </message>
<message>
<source>Cast &amp; Crew</source>
<translation>Cast &amp; Crew</translation>
</message>
<message>
<source>TV Shows</source>
<translation>Serie TV</translation>
</message>
<message>
<source>Wednesday</source>
<translation>Mercoledi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>Giovedi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started</source>
<translation>Iniziato</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Starts</source>
<translation>Inizia</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ends at</source>
<translation>Termina alle</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>TV Guide</source>
<translation>Guida TV</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
<message>
<source>Connecting to Server</source>
<translation>In Connessione al Server</translation>
<extracomment>Message to display to user while client is attempting to connect to the server</extracomment>
</message>
<message>
<source>The requested content does not exist on the server</source>
<translation>Il contenuto richiesto non esiste sul server</translation>
<extracomment>Content of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Inserisci il nome o l&apos;IP del server</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Scegli un server Jellyfin dalla rete locale</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>Se il server non è nella lista, puoi anche inserire l&apos;URL:</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>Error Getting Playback Information</source>
<translation>Errore nel recupero delle informazioni di riproduzione</translation>
<extracomment>Dialog Title: Received error from server when trying to get information about the selected item for playback</extracomment>
</message>
<message>
<source>Tuesday</source>
<translation>Martedi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>today</source>
<translation>oggi</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>Close</source>
<translation>Chiudi</translation>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Premi &quot;OK&quot; per Chiudere</translation>
</message>
<message>
<source>Movies</source>
<translation>Film</translation>
</message>
<message>
<source>yesterday</source>
<translation>ieri</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>tomorrow</source>
<translation>domani</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Sunday</source>
<translation>Domenica</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>Lunedi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Venerdi</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Sabato</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Iniziato il</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Inizia il</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Cancel Recording</source>
<translation>Interrompi Registrazione</translation>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Interrompi Registrazione Seria</translation>
</message>
<message>
<source>Not found</source>
<translation>Non trovato</translation>
<extracomment>Title of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<source>Ended at</source>
<translation>Terminato alle</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>Live</source>
<translation>In diretta</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Replica</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>Channels</source>
<translation>Canali</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>View Channel</source>
<translation>Visione del Canale</translation>
</message>
<message>
<source>Record</source>
<translation>Registra</translation>
</message>
<message>
<source>Unknown</source>
<translation>Sconosciuto</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Controllo Parentale</translation>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Scegli un server Jellyfin dalla rete locale:</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>Save Credentials?</source>
<translation>Salvare le credenziali?</translation>
</message>
<message>
<source>Change Server</source>
<translation>Cambia server</translation>
</message>
<message>
<source>Error During Playback</source>
<translation>Errore durante la riproduzione</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
</context> </context>
</TS> </TS>

View File

@ -2583,5 +2583,65 @@ não contém itens</translation>
<translation>Erro Durante Reprodução</translation> <translation>Erro Durante Reprodução</translation>
<extracomment>Dialog title when error occurs during playback</extracomment> <extracomment>Dialog title when error occurs during playback</extracomment>
</message> </message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>Houve um erro ao coletar dados do servidor para este item.</translation>
<extracomment>Dialog detail when unable to load Content from Server</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Carregando dados do canal</translation>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Este %1 não possui itens</translation>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Nome</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Data de Reprodução</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>Avaliação IMDb</translation>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Data de Lançamento</translation>
</message>
<message>
<source>An error was encountered while playing this item.</source>
<translation>Um erro foi encontrado enquanto reproduzindo este item.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Erro ao carregar os dados do canal</translation>
</message>
<message>
<source>Unable to load Channel Data from the server</source>
<translation>Não foi possível carregar do servidor os dados do canal</translation>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Avaliação de críticos</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Data de Adição</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Número de Reproduções</translation>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Classificação Etária</translation>
</message>
</context> </context>
</TS> </TS>

View File

@ -671,5 +671,292 @@
<source>Save Credentials?</source> <source>Save Credentials?</source>
<translation>Uložiť poverenia?</translation> <translation>Uložiť poverenia?</translation>
</message> </message>
<message>
<source>Save Credentials?</source>
<translation>Uložiť prihlasovacie údaje?</translation>
</message>
<message>
<source>Monday</source>
<translation>Pondelok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Začalo o</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>View Channel</source>
<translation>Zobraziť kanál</translation>
</message>
<message>
<source>Record Series</source>
<translation>Séria nahrávok</translation>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Zrušiť nahrávanie série</translation>
</message>
<message>
<source>Connecting to Server</source>
<translation>Pripája sa k serveru</translation>
<extracomment>Message to display to user while client is attempting to connect to the server</extracomment>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Zadajte názov servera alebo IP adresu</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>Error Getting Playback Information</source>
<translation>Chyba pri získavaní informácií o prehrávaní</translation>
<extracomment>Dialog Title: Received error from server when trying to get information about the selected item for playback</extracomment>
</message>
<message>
<source>An error was encountered while playing this item. Server did not provide required transcoding data.</source>
<translation>Pri prehrávaní tejto položky sa vyskytla chyba. Server neposkytol požadované údaje na prekódovanie.</translation>
<extracomment>Content of message box when trying to play an item which requires transcoding, and the server did not provide transcode url</extracomment>
</message>
<message>
<source>Live</source>
<translation>Naživo</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Piatok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Delete Saved</source>
<translation>Odstrániť uložené</translation>
</message>
<message>
<source>On Now</source>
<translation>Teraz</translation>
</message>
<message>
<source>DATE_PLAYED</source>
<translation>Dátum hrania</translation>
</message>
<message>
<source>TV Guide</source>
<translation>TV sprievodca</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
<message>
<source>Born</source>
<translation>Narodený</translation>
</message>
<message>
<source>Cast &amp; Crew</source>
<translation>Herci &amp; štáb</translation>
</message>
<message>
<source>More Like This</source>
<translation>Viac takých</translation>
</message>
<message>
<source>Movies</source>
<translation>Filmy</translation>
</message>
<message>
<source>Tuesday</source>
<translation>Utorok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Starts</source>
<translation>Začína</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ended at</source>
<translation>Skončil o</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>The requested content does not exist on the server</source>
<translation>Požadovaný obsah na serveri neexistuje</translation>
<extracomment>Content of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Meno</translation>
</message>
<message>
<source>PLAY_COUNT</source>
<translation>Počet prehrania</translation>
</message>
<message>
<source>Died</source>
<translation>Zomrel</translation>
</message>
<message>
<source>Age</source>
<translation>Vek</translation>
</message>
<message>
<source>Special Features</source>
<translation>Špeciálne vlastnosti</translation>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</translation>
</message>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Zatvorte stlačením tlačidla OK</translation>
</message>
<message>
<source>tomorrow</source>
<translation>zajtra</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Sunday</source>
<translation>Nedeľa</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>Streda</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Sobota</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started</source>
<translation>Začalo</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Začne</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Ends at</source>
<translation>Končí o</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Not found</source>
<translation>Nenájdené</translation>
<extracomment>Title of message box when the requested content is not found on the server</extracomment>
</message>
<message>
<comment>Title of Tab for options to sort library content</comment>
<source>TAB_SORT</source>
<translation>Triediť</translation>
</message>
<message>
<source>RUNTIME</source>
<translation>Beh programu</translation>
</message>
<message>
<source>Version</source>
<translation>Verzia</translation>
</message>
<message>
<comment>Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)</comment>
<source>NO_ITEMS</source>
<translation>Tento %1 neobsahuje žiadne položky</translation>
</message>
<message>
<source>IMDB_RATING</source>
<translation>IMDB hodnotenie</translation>
</message>
<message>
<source>DATE_ADDED</source>
<translation>Dátum pridania</translation>
</message>
<message>
<source>OFFICIAL_RATING</source>
<translation>Rodičovské hodnotenie</translation>
</message>
<message>
<source>RELEASE_DATE</source>
<translation>Dátum vydania</translation>
</message>
<message>
<comment>Title of Tab for switching &quot;views&quot; when looking at a library</comment>
<source>TAB_VIEW</source>
<translation>Vyhliadka</translation>
</message>
<message>
<comment>Title of Tab for options to filter library content</comment>
<source>TAB_FILTER</source>
<translation>Filter</translation>
</message>
<message>
<source>TV Shows</source>
<translation>TV relácie</translation>
</message>
<message>
<source>today</source>
<translation>dnes</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>yesterday</source>
<translation>včera</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>Štvrtok</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Opakujte</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>Channels</source>
<translation>Kanály</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>Record</source>
<translation>Záznam</translation>
</message>
<message>
<source>Cancel Recording</source>
<translation>Zrušiť nahrávanie</translation>
</message>
<message>
<source>Close</source>
<translation>Zavrieť</translation>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Vyberte dostupný server Jellyfin z vašej lokálnej siete:</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>Ak vyššie nie je uvedený žiadny server, adresu URL servera môžete zadať aj ručne:</translation>
<extracomment>Instructions on initial app launch when the user is asked to manually enter a server URL</extracomment>
</message>
<message>
<source>CRITIC_RATING</source>
<translation>Hodnotenie kritikov</translation>
</message>
<message>
<source>Unknown</source>
<translation>Neznámy</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>Playback</source>
<translation>Prehrávanie</translation>
<extracomment>Title for Playback section in user setting screen.</extracomment>
</message>
</context> </context>
</TS> </TS>

View File

@ -2,7 +2,7 @@
title=Jellyfin title=Jellyfin
major_version=1 major_version=1
minor_version=6 minor_version=6
build_version=1 build_version=2
### Main Menu Icons / Channel Poster Artwork ### Main Menu Icons / Channel Poster Artwork
mm_icon_focus_fhd=pkg:/images/channel-poster_fhd.png mm_icon_focus_fhd=pkg:/images/channel-poster_fhd.png

6804
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,11 @@
{ {
"name": "jellyfin-roku", "name": "jellyfin-roku",
"version": "1.6.1", "version": "1.6.2",
"description": "Roku app for Jellyfin media server", "description": "Roku app for Jellyfin media server",
"main": "index.js", "main": "index.js",
"directories": {
"test": "tests"
},
"devDependencies": { "devDependencies": {
"@rokucommunity/bslint": "0.7.5", "@rokucommunity/bslint": "0.7.5",
"brighterscript": "0.57.2", "brighterscript": "0.61.1",
"rooibos-cli": "1.4.0",
"ropm": "0.10.10" "ropm": "0.10.10"
}, },
"scripts": { "scripts": {

View File

@ -3,6 +3,13 @@
"title": "Playback", "title": "Playback",
"description": "Settings relating to playback and supported codec and media types.", "description": "Settings relating to playback and supported codec and media types.",
"children": [ "children": [
{
"title": "AV1 Support",
"description": "** EXPERIMENTAL** Support Direct Play of AV1 content if this Roku device supports it.",
"settingName": "playback.av1",
"type": "bool",
"default": "false"
},
{ {
"title": "MPEG-2 Support", "title": "MPEG-2 Support",
"description": "Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.", "description": "Support Direct Play of MPEG-2 content (e.g., Live TV). This will prevent transcoding of MPEG-2 content, but uses significantly more bandwidth.",
@ -11,12 +18,26 @@
"default": "false" "default": "false"
}, },
{ {
"title": "Attempt Direct Play (Profile Lvl)", "title": "MPEG-4 Support",
"description": "Attempt Direct Play for H.264 media with unsupported profile levels (> 4.2) before falling back to transcoding if it fails.", "description": "Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.",
"settingName": "playback.mpeg4",
"type": "bool",
"default": "true"
},
{
"title": "Direct Play H.264 Unsupported Profile Levels",
"description": "Attempt Direct Play for H.264 media with unsupported profile levels before falling back to transcoding if it fails.",
"settingName": "playback.tryDirect.h264ProfileLevel", "settingName": "playback.tryDirect.h264ProfileLevel",
"type": "bool", "type": "bool",
"default": "true" "default": "true"
}, },
{
"title": "Direct Play HEVC Unsupported Profile Levels",
"description": "Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.",
"settingName": "playback.tryDirect.hevcProfileLevel",
"type": "bool",
"default": "true"
},
{ {
"title": "Cinema Mode", "title": "Cinema Mode",
"description": "Cinema Mode brings the theater experience straight to your living room with the ability to play custom intros before the main feature.", "description": "Cinema Mode brings the theater experience straight to your living room with the ability to play custom intros before the main feature.",

View File

@ -1,9 +1,5 @@
sub Main (args as dynamic) as void sub Main (args as dynamic) as void
' If the Rooibos files are included in deployment, run tests
'bs:disable-next-line
if type(Rooibos__Init) = "Function" then Rooibos__Init()
' The main function that runs when the application is launched. ' The main function that runs when the application is launched.
m.screen = CreateObject("roSGScreen") m.screen = CreateObject("roSGScreen")
@ -13,13 +9,6 @@ sub Main (args as dynamic) as void
WriteAsciiFile("tmp:/scene.temp", "") WriteAsciiFile("tmp:/scene.temp", "")
MoveFile("tmp:/scene.temp", "tmp:/scene") MoveFile("tmp:/scene.temp", "tmp:/scene")
' Temporary code to migrate MPEG2 setting from device setting to user setting
' Added for 1.4.13 release and should probably be removed for 1.4.15
if get_setting("playback.mpeg2") <> invalid and registry_read("playback.mpeg2", get_setting("active_user")) = invalid
set_user_setting("playback.mpeg2", get_setting("playback.mpeg2"))
end if
' End Temporary code
m.port = CreateObject("roMessagePort") m.port = CreateObject("roMessagePort")
m.screen.setMessagePort(m.port) m.screen.setMessagePort(m.port)
m.scene = m.screen.CreateScene("JFScene") m.scene = m.screen.CreateScene("JFScene")
@ -110,8 +99,20 @@ sub Main (args as dynamic) as void
else if isNodeEvent(msg, "selectedItem") else if isNodeEvent(msg, "selectedItem")
' If you select a library from ANYWHERE, follow this flow ' If you select a library from ANYWHERE, follow this flow
selectedItem = msg.getData() selectedItem = msg.getData()
m.selectedItemType = selectedItem.type m.selectedItemType = selectedItem.type
if selectedItem.type = "CollectionFolder" or selectedItem.type = "UserView" or selectedItem.type = "Folder" or selectedItem.type = "Channel" or selectedItem.type = "Boxset" '
if selectedItem.type = "CollectionFolder"
if selectedItem.collectionType = "movies"
group = CreateMovieLibraryView(selectedItem)
else
group = CreateItemGrid(selectedItem)
end if
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "Folder" and selectedItem.json.type = "Genre"
group = CreateMovieLibraryView(selectedItem)
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "UserView" or selectedItem.type = "Folder" or selectedItem.type = "Channel" or selectedItem.type = "Boxset"
group = CreateItemGrid(selectedItem) group = CreateItemGrid(selectedItem)
sceneManager.callFunc("pushScene", group) sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "Episode" else if selectedItem.type = "Episode"
@ -128,6 +129,8 @@ sub Main (args as dynamic) as void
end if end if
else if selectedItem.type = "Series" else if selectedItem.type = "Series"
group = CreateSeriesDetailsGroup(selectedItem.json) group = CreateSeriesDetailsGroup(selectedItem.json)
else if selectedItem.type = "Season"
group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.id)
else if selectedItem.type = "Movie" else if selectedItem.type = "Movie"
' open movie detail page ' open movie detail page
group = CreateMovieDetailsGroup(selectedItem) group = CreateMovieDetailsGroup(selectedItem)
@ -142,7 +145,12 @@ sub Main (args as dynamic) as void
dialog.title = tr("Loading Channel Data") dialog.title = tr("Loading Channel Data")
m.scene.dialog = dialog m.scene.dialog = dialog
if LCase(selectedItem.subtype()) = "extrasdata"
video = CreateVideoPlayerGroup(video_id, invalid, 1, false, true, false)
else
video = CreateVideoPlayerGroup(video_id) video = CreateVideoPlayerGroup(video_id)
end if
dialog.close = true dialog.close = true
if video <> invalid and video.errorMsg <> "introaborted" if video <> invalid and video.errorMsg <> "introaborted"
@ -332,7 +340,7 @@ sub Main (args as dynamic) as void
video_id = trailerData[0].id video_id = trailerData[0].id
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx) video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx, false, false)
if video <> invalid and video.errorMsg <> "introaborted" if video <> invalid and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video) sceneManager.callFunc("pushScene", video)
end if end if
@ -437,12 +445,6 @@ sub Main (args as dynamic) as void
autoPlayNextEpisode(node.id, node.showID) autoPlayNextEpisode(node.id, node.showID)
end if end if
end if end if
'else if isNodeEvent(msg, "selectedExtra")
'rl = msg.getData()
'sel = rl.rowItemSelected
'? "msg.getfield():" + msg.getField()
'stop
'CreatePersonView(msg.getData())
else if type(msg) = "roDeviceInfoEvent" else if type(msg) = "roDeviceInfoEvent"
event = msg.GetInfo() event = msg.GetInfo()
group = sceneManager.callFunc("getActiveScene") group = sceneManager.callFunc("getActiveScene")

View File

@ -352,7 +352,7 @@ function CreateMovieDetailsGroup(movie)
extras = group.findNode("extrasGrid") extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port) extras.observeField("selectedItem", m.port)
extras.callFunc("loadPeople", movie.json) extras.callFunc("loadParts", movie.json)
return group return group
end function end function
@ -369,7 +369,7 @@ function CreateSeriesDetailsGroup(series)
extras = group.findNode("extrasGrid") extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port) extras.observeField("selectedItem", m.port)
extras.callFunc("loadPeople", group.itemcontent.json) extras.callFunc("loadParts", group.itemcontent.json)
return group return group
end function end function
@ -453,6 +453,20 @@ function CreateSeasonDetailsGroup(series, season)
return group return group
end function end function
function CreateSeasonDetailsGroupByID(seriesID, seasonID)
group = CreateObject("roSGNode", "TVEpisodes")
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
group.seasonData = ItemMetaData(seasonID).json
group.objects = TVEpisodes(seriesID, seasonID)
group.observeField("episodeSelected", m.port)
group.observeField("quickPlayNode", m.port)
return group
end function
function CreateItemGrid(libraryItem) function CreateItemGrid(libraryItem)
group = CreateObject("roSGNode", "ItemGrid") group = CreateObject("roSGNode", "ItemGrid")
group.parentItem = libraryItem group.parentItem = libraryItem
@ -461,6 +475,14 @@ function CreateItemGrid(libraryItem)
return group return group
end function end function
function CreateMovieLibraryView(libraryItem)
group = CreateObject("roSGNode", "MovieLibraryView")
group.parentItem = libraryItem
group.optionsAvailable = true
group.observeField("selectedItem", m.port)
return group
end function
function CreateSearchPage() function CreateSearchPage()
' Search + Results Page ' Search + Results Page
group = CreateObject("roSGNode", "searchResults") group = CreateObject("roSGNode", "searchResults")
@ -476,10 +498,10 @@ sub CreateSidePanel(buttons, options)
group.options = options group.options = options
end sub end sub
function CreateVideoPlayerGroup(video_id, mediaSourceId = invalid, audio_stream_idx = 1, forceTranscoding = false, showIntro = true) function CreateVideoPlayerGroup(video_id, mediaSourceId = invalid, audio_stream_idx = 1, forceTranscoding = false, showIntro = true, allowResumeDialog = true)
' Video is Playing ' Video is Playing
video = VideoPlayer(video_id, mediaSourceId, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), forceTranscoding, showIntro) video = VideoPlayer(video_id, mediaSourceId, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), forceTranscoding, showIntro, allowResumeDialog)
if video = invalid then return invalid if video = invalid then return invalid
if video.errorMsg = "introaborted" then return video if video.errorMsg = "introaborted" then return video
@ -570,17 +592,6 @@ function CreatePersonView(personData as object) as object
return person return person
end function end function
function CreatePhotoPage(photo)
group = CreateObject("roSGNode", "PhotoDetails")
group.optionsAvailable = true
m.global.sceneManager.callFunc("pushScene", group)
group.itemContent = photo
return group
end function
sub UpdateSavedServerList() sub UpdateSavedServerList()
server = get_setting("server") server = get_setting("server")
username = get_setting("username") username = get_setting("username")

View File

@ -1,8 +1,8 @@
function VideoPlayer(id, mediaSourceId = invalid, audio_stream_idx = 1, subtitle_idx = -1, forceTranscoding = false, showIntro = true) function VideoPlayer(id, mediaSourceId = invalid, audio_stream_idx = 1, subtitle_idx = -1, forceTranscoding = false, showIntro = true, allowResumeDialog = true)
' Get video controls and UI ' Get video controls and UI
video = CreateObject("roSGNode", "JFVideo") video = CreateObject("roSGNode", "JFVideo")
video.id = id video.id = id
AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, -1, forceTranscoding, showIntro) AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, -1, forceTranscoding, showIntro, allowResumeDialog)
if video.errorMsg = "introaborted" if video.errorMsg = "introaborted"
return video return video
@ -19,7 +19,7 @@ function VideoPlayer(id, mediaSourceId = invalid, audio_stream_idx = 1, subtitle
return video return video
end function end function
sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -1, playbackPosition = -1, forceTranscoding = false, showIntro = true) sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -1, playbackPosition = -1, forceTranscoding = false, showIntro = true, allowResumeDialog = true)
video.content = createObject("RoSGNode", "ContentNode") video.content = createObject("RoSGNode", "ContentNode")
meta = ItemMetaData(video.id) meta = ItemMetaData(video.id)
m.videotype = meta.type m.videotype = meta.type
@ -45,11 +45,17 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
end if end if
end if end if
if m.videotype = "Episode" or m.videotype = "Series"
video.runTime = (meta.json.RunTimeTicks / 10000000.0)
video.content.contenttype = "episode"
end if
video.content.title = meta.title video.content.title = meta.title
video.showID = meta.showID video.showID = meta.showID
if playbackPosition = -1 if playbackPosition = -1
playbackPosition = meta.json.UserData.PlaybackPositionTicks playbackPosition = meta.json.UserData.PlaybackPositionTicks
if allowResumeDialog
if playbackPosition > 0 if playbackPosition > 0
dialogResult = startPlayBackOver(playbackPosition) dialogResult = startPlayBackOver(playbackPosition)
'Dialog returns -1 when back pressed, 0 for resume, and 1 for start over 'Dialog returns -1 when back pressed, 0 for resume, and 1 for start over
@ -150,6 +156,7 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
end if end if
end if end if
end if end if
end if
' Don't attempt to play an intro for an intro video ' Don't attempt to play an intro for an intro video
if showIntro if showIntro
@ -213,18 +220,22 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
fully_external = false fully_external = false
' For h264 video, Roku spec states that it supports and Encoding level 4.1 and 4.2. ' 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 ' 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 ' artifacts. If the user preference is set, and the only reason the server says we need to
' transcode is that the Envoding Level is not supported, then try to direct play but silently ' transcode is that the Encoding Level is not supported, then try to direct play but silently
' fall back to the transcode if that fails. ' fall back to the transcode if that fails.
if meta.live = false and get_user_setting("playback.tryDirect.h264ProfileLevel") = "true" and m.playbackInfo.MediaSources[0].TranscodingUrl <> invalid and forceTranscoding = false and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "h264" 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
transcodingReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl) transcodingReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl)
if transcodingReasons.Count() = 1 and transcodingReasons[0] = "VideoLevelNotSupported" if transcodingReasons.Count() = 1 and transcodingReasons[0] = "VideoLevelNotSupported"
video.directPlaySupported = true video.directPlaySupported = true
video.transcodeAvailable = true video.transcodeAvailable = true
end if end if
end if end if
end if
if video.directPlaySupported if video.directPlaySupported
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol) protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
@ -420,7 +431,7 @@ sub autoPlayNextEpisode(videoID as string, showID as string)
if data <> invalid and data.Items.Count() = 2 if data <> invalid and data.Items.Count() = 2
' setup new video node ' setup new video node
nextVideo = CreateVideoPlayerGroup(data.Items[1].Id, invalid, 1, false, false) nextVideo = CreateVideoPlayerGroup(data.Items[1].Id, invalid, 1, false, false)
' remove last video scene ' remove last videoplayer scene
m.global.sceneManager.callFunc("clearPreviousScene") m.global.sceneManager.callFunc("clearPreviousScene")
if nextVideo <> invalid if nextVideo <> invalid
m.global.sceneManager.callFunc("pushScene", nextVideo) m.global.sceneManager.callFunc("pushScene", nextVideo)

View File

@ -329,26 +329,32 @@ function AudioStream(id as string)
songData = AudioItem(id) songData = AudioItem(id)
content = createObject("RoSGNode", "ContentNode") content = createObject("RoSGNode", "ContentNode")
params = {}
params.append({
"Static": "true",
"Container": songData.mediaSources[0].container
})
params.MediaSourceId = songData.mediaSources[0].id
content.url = buildURL(Substitute("Audio/{0}/stream", songData.id), params)
content.title = songData.title content.title = songData.title
content.streamformat = songData.mediaSources[0].container
playbackInfo = ItemPostPlaybackInfo(songData.id, params.MediaSourceId) playbackInfo = ItemPostPlaybackInfo(songData.id, songData.mediaSources[0].id)
content.id = playbackInfo.PlaySessionId 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
return content return content
end function end function
function useTranscodeAudioStream(playbackInfo)
return playbackInfo.mediaSources[0] <> invalid and playbackInfo.mediaSources[0].TranscodingURL <> invalid
end function
function BackdropImage(id as string) function BackdropImage(id as string)
imgParams = { "maxHeight": "720", "maxWidth": "1280" } imgParams = { "maxHeight": "720", "maxWidth": "1280" }
return ImageURL(id, "Backdrop", imgParams) return ImageURL(id, "Backdrop", imgParams)

View File

@ -18,6 +18,7 @@ end function
function getDeviceProfile() as object function getDeviceProfile() as object
playMpeg2 = get_user_setting("playback.mpeg2") = "true" playMpeg2 = get_user_setting("playback.mpeg2") = "true"
playAv1 = get_user_setting("playback.av1") = "true"
'Check if 5.1 Audio Output connected 'Check if 5.1 Audio Output connected
maxAudioChannels = 2 maxAudioChannels = 2
@ -26,14 +27,19 @@ function getDeviceProfile() as object
maxAudioChannels = 6 maxAudioChannels = 6
end if end if
if playMpeg2 and di.CanDecodeVideo({ Codec: "mpeg2" }).Result = true addHevcProfile = false
tsVideoCodecs = "h264,mpeg2video" MAIN10 = ""
else
tsVideoCodecs = "h264" tsVideoCodecs = "h264"
if di.CanDecodeVideo({ Codec: "hevc" }).Result = true
tsVideoCodecs = "h265,hevc," + tsVideoCodecs
addHevcProfile = true
if di.CanDecodeVideo({ Codec: "hevc", Profile: "main 10" }).Result
MAIN10 = "|main 10"
end if
end if end if
if di.CanDecodeVideo({ Codec: "hevc" }).Result = true if playMpeg2 and di.CanDecodeVideo({ Codec: "mpeg2" }).Result = true
tsVideoCodecs = tsVideoCodecs + ",h265,hevc" tsVideoCodecs = tsVideoCodecs + ",mpeg2video"
end if end if
if di.CanDecodeAudio({ Codec: "ac3" }).result if di.CanDecodeAudio({ Codec: "ac3" }).result
@ -42,9 +48,42 @@ function getDeviceProfile() as object
tsAudioCodecs = "aac" tsAudioCodecs = "aac"
end if end if
addAv1Profile = false
if playAv1 and di.CanDecodeVideo({ Codec: "av1" }).result
tsVideoCodecs = tsVideoCodecs + ",av1"
addAv1Profile = true
end if
addVp9Profile = false
if di.CanDecodeVideo({ Codec: "vp9" }).result
tsVideoCodecs = tsVideoCodecs + ",vp9"
addVp9Profile = true
end if
hevcVideoRangeTypes = "SDR"
vp9VideoRangeTypes = "SDR"
av1VideoRangeTypes = "SDR"
dp = di.GetDisplayProperties()
if dp.Hdr10 ' or dp.Hdr10Plus?
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HDR10"
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HDR10"
av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10"
end if
if dp.HLG
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HLG"
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HLG"
av1VideoRangeTypes = av1VideoRangeTypes + "|HLG"
end if
if dp.DolbyVision
hevcVideoRangeTypes = hevcVideoRangeTypes + "|DOVI"
'vp9VideoRangeTypes = vp9VideoRangeTypes + ",DOVI" no evidence that vp9 can hold DOVI
av1VideoRangeTypes = av1VideoRangeTypes + "|DOVI"
end if
DirectPlayProfile = GetDirectPlayProfiles() DirectPlayProfile = GetDirectPlayProfiles()
return { deviceProfile = {
"MaxStreamingBitrate": 120000000, "MaxStreamingBitrate": 120000000,
"MaxStaticBitrate": 100000000, "MaxStaticBitrate": 100000000,
"MusicStreamingTranscodingBitrate": 192000, "MusicStreamingTranscodingBitrate": 192000,
@ -133,24 +172,6 @@ function getDeviceProfile() as object
"IsRequired": false "IsRequired": false
} }
] ]
},
{
"Type": "Video",
"Codec": "hevc",
"Conditions": [
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
"Value": "main",
"IsRequired": false
},
{
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": StrI(120 * 5.1),
"IsRequired": false
}
]
} }
], ],
"SubtitleProfiles": [ "SubtitleProfiles": [
@ -172,12 +193,68 @@ function getDeviceProfile() as object
} }
] ]
} }
if addAv1Profile
deviceProfile.CodecProfiles.push({
"Type": "Video",
"Codec": "av1",
"Conditions": [
{
"Condition": "EqualsAny",
"Property": "VideoRangeType",
"Value": av1VideoRangeTypes,
"IsRequired": false
}
]
})
end if
if addHevcProfile
deviceProfile.CodecProfiles.push({
"Type": "Video",
"Codec": "hevc",
"Conditions": [
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
"Value": "main" + MAIN10,
"IsRequired": false
},
{
"Condition": "EqualsAny",
"Property": "VideoRangeType",
"Value": hevcVideoRangeTypes,
"IsRequired": false
},
{
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": (120 * 5.1).ToStr(),
"IsRequired": false
}
]
})
end if
if addVp9Profile
deviceProfile.CodecProfiles.push({
"Type": "Video",
"Codec": "vp9",
"Conditions": [
{
"Condition": "EqualsAny",
"Property": "VideoRangeType",
"Value": vp9VideoRangeTypes,
"IsRequired": false
}
]
})
end if
return deviceProfile
end function end function
function GetDirectPlayProfiles() as object function GetDirectPlayProfiles() as object
mp4Video = "h264,mpeg4" mp4Video = "h264"
mp4Audio = "mp3,pcm,lpcm,wav" mp4Audio = "mp3,pcm,lpcm,wav"
mkvVideo = "h264,vp8" mkvVideo = "h264,vp8"
mkvAudio = "mp3,pcm,lpcm,wav" mkvAudio = "mp3,pcm,lpcm,wav"
@ -202,6 +279,10 @@ function GetDirectPlayProfiles() as object
mkvVideo = mkvVideo + ",mpeg2video" mkvVideo = mkvVideo + ",mpeg2video"
end if end if
if get_user_setting("playback.mpeg4") = "true"
mp4Video = mp4Video + ",mpeg4"
end if
' Check for supported Audio ' Check for supported Audio
if di.CanDecodeAudio({ Codec: "ac3" }).result if di.CanDecodeAudio({ Codec: "ac3" }).result
mkvAudio = mkvAudio + ",ac3" mkvAudio = mkvAudio + ",ac3"

View File

@ -1,6 +0,0 @@
{
"testPath": "out/staging/source/tests/specs",
"rootPath": "",
"outputPath": "out/staging/source/tests/rooibos",
"projectPath": "."
}

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component
name="TestsScene"
extends="Scene"
xsi:noNamespaceSchemaLocation="https://devtools.web.roku.com/schema/RokuSceneGraph.xsd"
>
<interface>
<function name="Rooibos_CreateTestNode" />
</interface>
<script type="text/brightscript" uri="pkg:/source/tests/rooibos/rooibosDist.brs" />
</component>

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +0,0 @@
'@TestSuite [GLT] Globals Tests
'@Setup
function GLT_setup() as void
end function
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
'@It tests the ability to use the globals getter/setter
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
'@Test Global Getter and Setter
'@Params["this", "that"]
'@Params["somenumber", 2342342]
function GLT__SetGetGlobals(key, value) as void
setGlobal(key, value)
m.assertEqual(getGlobal(key), value)
end function

View File

@ -1,8 +0,0 @@
{
"logLevel": 4,
"testsDirectory": "pkg:source/tests/specs",
"failFast": false,
"showOnlyFailures": false,
"maxLinesWithoutSuiteDirective": 30,
"supportLegacyTests": false
}