Merge pull request #1198 from cewert/unstable

This commit is contained in:
Charles Ewert 2023-04-14 11:37:23 -04:00 committed by GitHub
commit 86305e420b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 10931 additions and 10257 deletions

4
.vscode/launch.json vendored
View File

@ -6,6 +6,10 @@
"request": "launch",
"name": "Jellyfin Debug: Launch",
"stopOnEntry": false,
// To enable RALE:
// set "brightscript.debug.raleTrackerTaskFileLocation": "/absolute/path/to/rale/TrackerTask.xml" in your vscode user settings
// set the below field to true
"injectRaleTrackerTask": false,
//WARNING: don't edit this value. Instead, set "brightscript.debug.host": "YOUR_HOST_HERE" in your vscode user settings
//"host": "${promptForHost}",
//WARNING: don't edit this value. Instead, set "brightscript.debug.password": "YOUR_PASSWORD_HERE" in your vscode user settings

View File

@ -27,7 +27,7 @@ Follow the steps below to install the app on your personal Roku device. This wil
## Developer Mode
Put your Roku device in [developer mode](https://blog.roku.com/developer/2016/02/04/developer-setup-guide). Write down your Roku device IP and the password you created, you will need these later.
Put your Roku device in [developer mode](https://blog.roku.com/developer/2016/02/04/developer-setup-guide). Write down your Roku device IP and the password you created - you will need these!
## Clone the GitHub Repo
@ -71,7 +71,7 @@ That's it! VSCode will auto-package the project, sideload it to the specified de
Out of the box, the BrightScript extension will prompt you to pick a Roku device (from devices found on your local network) and enter a password on every launch. If you'd prefer to hardcode this information rather than entering it every time, you can set these values in your VSCode user settings:
```js
```json
{
"brightscript.debug.host": "YOUR_ROKU_HOST_HERE",
"brightscript.debug.password": "YOUR_ROKU_DEV_PASSWORD_HERE",

View File

@ -10,7 +10,7 @@
##########################################################################
APPNAME = Jellyfin_Roku
VERSION = 1.6.4
VERSION = 1.6.5
ZIP_EXCLUDE= -x xml/* -x artwork/* -x \*.pkg -x storeassets\* -x keys\* -x \*/.\* -x *.git* -x *.DS* -x *.pkg* -x dist/**\* -x out/**\*

View File

@ -1,39 +1,41 @@
<h1 style="text-align: center;">Jellyfin app for Roku</h1>
<h3 style="text-align: center;">Part of the <a href="https://jellyfin.media/">Jellyfin</a> Project</h3>
<h1 align="center">Jellyfin Roku</h1>
<h2 align="center">Part of the <a href="https://jellyfin.org">Jellyfin Project</a></h2>
<p align="center">
<img alt="Logo banner" src="https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/banner-logo-solid.svg?sanitize=true"/>
<br/><br/>
<a href="https://github.com/jellyfin/jellyfin-roku">
<img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin-roku.svg"/>
</a>
<a href="https://github.com/jellyfin/jellyfin-roku/releases">
<img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin-roku.svg"/>
</a>
<a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-roku/?utm_source=widget">
<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-roku/svg-badge.svg" alt="Translation status" />
</a>
<br/>
<a href="https://matrix.to/#/#jellyfin-dev-roku:matrix.org">
<img alt="Chat on Matrix" src="https://img.shields.io/matrix/jellyfin:matrix.org.svg?logo=matrix"/>
</a>
<a href="https://www.reddit.com/r/jellyfin">
<img alt="Join our Subreddit" src="https://img.shields.io/badge/reddit-r%2Fjellyfin-%23FF5700.svg"/>
</a>
</p>
[![Logo Banner](https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/banner-logo-solid.svg?sanitize=true "Jellyfin")](https://jellyfin.org)
The Jellyfin Roku App is a Jellyfin client for Roku Devices. This is still very much a work in progress, so we would encourage you to [get involved](#get_involved) if you can.
[![Build Status](https://img.shields.io/github/actions/workflow/status/jellyfin/jellyfin-roku/build-dev.yml?logo=github&branch=unstable "Build Status")](https://github.com/jellyfin/jellyfin-roku/actions/workflows/build-dev.yml?query=branch%3Aunstable)
[![Current Release](https://img.shields.io/github/release/jellyfin/jellyfin-roku.svg?logo=github "Current Release")](https://github.com/jellyfin/jellyfin-roku/releases)
[![Translation Status](https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-roku/svg-badge.svg "Translation Status")](https://translate.jellyfin.org/projects/jellyfin/jellyfin-roku/?utm_source=widget)
[![Matrix](https://img.shields.io/matrix/jellyfin:matrix.org.svg?logo=matrix "Chat on Matrix")](https://matrix.to/#/#jellyfin-dev-roku:matrix.org)
[![Reddit](https://img.shields.io/badge/reddit-r%2Fjellyfin-%23FF5700.svg?logo=reddit "Join our Subreddit")](https://www.reddit.com/r/jellyfin)
[![License](https://img.shields.io/github/license/jellyfin/jellyfin-roku.svg "GPL 2.0 License")](LICENSE)
## Getting Started
Jellyfin Roku is the official Jellyfin client for Roku devices. We welcome all contributions and pull requests! If you have a larger feature in mind please [open an issue](https://github.com/jellyfin/jellyfin-roku/issues/new?assignees=&labels=feature&template=feature_request.md&title=) so we can discuss the implementation before you start.
The channel is available on the [Roku Channel Store](https://channelstore.roku.com/details/cc5e559d08d9ec87c5f30dcebdeebc12/jellyfin).
## Install
## Getting Involved<a name="get_involved"></a>
Download the latest release on the [Roku Channel Store](https://channelstore.roku.com/details/cc5e559d08d9ec87c5f30dcebdeebc12/jellyfin).
No matter what your interests or skill are, you can help to make this client better for everyone by simply using the client and letting us know if you find a problem with it. Either give us a shout on [matrix](https://matrix.to/#/+jellyfin:matrix.org) or create a GitHub issue.
## Get Involved
Feature requests are always welcome too, but please have a read though the existing issues to see if someone has already raised one for something similar.
No matter what your interests or skills are you can help make this client better for everyone by simply using the client and giving feedback to the developers when things break. [Create an issue](https://github.com/jellyfin/jellyfin-roku/issues/new/choose) here on GitHub or give us a shout on [Matrix](https://matrix.to/#/#jellyfin-dev-roku:matrix.org).
If you fancy some development, then read the [DEVGUIDE](DEVGUIDE.md) to find out the best ways to help.
## Beta Test
As Roku have severely limited their Beta channel program, the best way to test pre-release versions is by following the [DEVGUIDE](DEVGUIDE.md) to install and test the latest changes. Feedback is always welcome.
To test the latest features before they get released:
1. Put your Roku device in [developer mode](https://blog.roku.com/developer/2016/02/04/developer-setup-guide). Write down your Roku device IP and the password you created - you will need these!
2. Download the [latest build](https://github.com/jellyfin/jellyfin-roku/actions/workflows/build-dev.yml?query=branch%3Aunstable). Select the first item listed then click the link at the bottom of the page i.e. `Jellyfin-Roku-dev-d3352495c579f6adeca085cdbc137ac36e70d558`. This will download a zip file to your computer.
3. Put your Roku's IP from step 1 into a browser i.e. `http://192.168.1.2` and press enter.
4. Log in with credentials from step 1.
5. Upload and install the zip file downloaded in step 2.
> NOTE: The beta app will always be at the bottom of your Roku's channel list and it will *not* automatically update.
## Advanced
For more advanced deployment methods, access to crash logs, or to learn how to setup a developer environment so you can write some code yourself please read the [DEVGUIDE](DEVGUIDE.md).
## Feature Requests
New feature requests are always welcome but before creating an issue please read through the [existing issues](https://github.com/jellyfin/jellyfin-roku/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) to see if someone has already raised one for what you're looking for.

View File

@ -44,7 +44,6 @@ sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtit
videotype = LCase(meta.type)
if videotype = "episode" or videotype = "series"
video.runTime = (meta.json.RunTimeTicks / 10000000.0)
video.content.contenttype = "episode"
end if

View File

@ -26,8 +26,10 @@ sub init()
m.overlayMinutes = m.top.findNode("overlayMinutes")
m.overlayMeridian = m.top.findNode("overlayMeridian")
m.overlayMeridian.font.size = 20
' start timer
m.currentTimeTimer = m.top.findNode("currentTimeTimer")
' display current time
updateTime()
' start timer to update clock every minute
m.currentTimeTimer.control = "start"
m.currentTimeTimer.ObserveField("fire", "updateTime")
end if

View File

@ -20,6 +20,11 @@ sub init()
m.nextEpisodeButton.text = tr("Next Episode")
m.nextEpisodeButton.setFocus(false)
m.nextupbuttonseconds = get_user_setting("playback.nextupbuttonseconds", "30")
if isValid(m.nextupbuttonseconds)
m.nextupbuttonseconds = val(m.nextupbuttonseconds)
else
m.nextupbuttonseconds = 30
end if
m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
@ -28,8 +33,6 @@ sub init()
m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask")
m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded")
m.top.observeField("state", "onState")
m.top.observeField("content", "onContentChange")
m.top.observeField("allowCaptions", "onAllowCaptionsChange")
end sub
@ -89,7 +92,6 @@ end sub
'
' Runs Next Episode button animation and sets focus to button
sub showNextEpisodeButton()
if m.top.content.contenttype <> 4 then return
if m.global.userConfig.EnableNextEpisodeAutoPlay and not m.nextEpisodeButton.visible
m.showNextEpisodeButtonAnimation.control = "start"
m.nextEpisodeButton.setFocus(true)
@ -100,7 +102,7 @@ end sub
'
'Update count down text
sub updateCount()
nextEpisodeCountdown = Int(m.top.runTime - m.top.position)
nextEpisodeCountdown = Int(m.top.duration - m.top.position)
if nextEpisodeCountdown < 0
nextEpisodeCountdown = 0
end if
@ -118,8 +120,9 @@ end sub
' Checks if we need to display the Next Episode button
sub checkTimeToDisplayNextEpisode()
if m.top.content.contenttype <> 4 then return
if m.nextupbuttonseconds = 0 then return
if int(m.top.position) >= (m.top.runTime - 30)
if int(m.top.position) >= (m.top.duration - m.nextupbuttonseconds)
showNextEpisodeButton()
updateCount()
return

View File

@ -22,8 +22,6 @@
<field id="videoId" type="string" />
<field id="mediaSourceId" type="string" />
<field id="audioIndex" type="integer" />
<field id="runTime" type="integer" />
</interface>
<script type="text/brightscript" uri="JFVideo.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />

View File

@ -83,18 +83,6 @@ sub onLibrariesLoaded()
haveLiveTV = false
' Load the NextUp Data
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
m.LoadNextUpTask.control = "RUN"
' Load the Continue Watching Data
m.LoadContinueTask.observeField("content", "updateContinueItems")
m.LoadContinueTask.control = "RUN"
' Load the Favorites Data
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
m.LoadFavoritesTask.control = "RUN"
' validate library data
if isValid(m.libraryData) and m.libraryData.count() > 0
userConfig = m.global.userConfig
@ -112,42 +100,42 @@ sub onLibrariesLoaded()
latestInRow = content.CreateChild("HomeRow")
latestInRow.title = tr("Latest in") + " " + lib.name + " >"
sizeArray.Push([464, 331])
loadLatest = createObject("roSGNode", "LoadItemsTask")
loadLatest.itemsToLoad = "latest"
loadLatest.itemId = lib.id
metadata = { "title": lib.name }
metadata.Append({ "contentType": lib.json.CollectionType })
loadLatest.metadata = metadata
loadLatest.observeField("content", "updateLatestItems")
loadLatest.control = "RUN"
else if lib.collectionType = "livetv"
' If we have Live TV, add "On Now"
onNowRow = content.CreateChild("HomeRow")
onNowRow.title = tr("On Now")
sizeArray.Push([464, 331])
haveLiveTV = true
' If we have Live TV access, load "On Now" data
if haveLiveTV
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
m.LoadOnNowTask.control = "RUN"
end if
end if
end for
end if
m.top.rowItemSize = sizeArray
m.top.content = content
' Load the Continue Watching Data
m.LoadContinueTask.observeField("content", "updateContinueItems")
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 haveLiveTV
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
m.LoadOnNowTask.control = "RUN"
end if
end sub
sub updateHomeRows()
if m.global.playstateTask.state = "run"
m.global.playstateTask.observeField("state", "updateHomeRows")
else
m.global.playstateTask.unobserveField("state")
return
end if
m.global.playstateTask.unobserveField("state")
m.LoadContinueTask.observeField("content", "updateContinueItems")
m.LoadContinueTask.control = "RUN"
end sub
@ -237,6 +225,9 @@ sub updateContinueItems()
homeRows.replaceChild(row, continueRowIndex)
end if
end if
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
m.LoadNextUpTask.control = "RUN"
end sub
sub updateNextUpItems()
@ -289,6 +280,24 @@ sub updateNextUpItems()
m.global.app_loaded = true
end if
' create task nodes for "Latest In" rows
userConfig = m.global.userConfig
filteredLatest = filterNodeArray(m.libraryData, "id", userConfig.LatestItemsExcludes)
for each lib in filteredLatest
if lib.collectionType <> "livetv" and lib.collectionType <> "boxsets" and lib.json.CollectionType <> "Program"
loadLatest = createObject("roSGNode", "LoadItemsTask")
loadLatest.itemsToLoad = "latest"
loadLatest.itemId = lib.id
metadata = { "title": lib.name }
metadata.Append({ "contentType": lib.json.CollectionType })
loadLatest.metadata = metadata
loadLatest.observeField("content", "updateLatestItems")
loadLatest.control = "RUN"
end if
end for
end sub
sub updateLatestItems(msg)

View File

@ -1,7 +1,9 @@
sub init()
m.queue = []
m.originalQueue = []
m.queueTypes = []
m.position = 0
m.shuffleEnabled = false
end sub
' Clear all content from play queue
@ -27,6 +29,11 @@ function getCurrentItem()
return getItemByIndex(m.position)
end function
' Return whether or not shuffle is enabled
function getIsShuffled()
return m.shuffleEnabled
end function
' Return the item in the passed index from the play queue
function getItemByIndex(index)
return m.queue[index]
@ -108,6 +115,54 @@ sub setPosition(newPosition)
m.position = newPosition
end sub
' Reset shuffle to off state
sub resetShuffle()
m.shuffleEnabled = false
end sub
' Toggle shuffleEnabled state
sub toggleShuffle()
m.shuffleEnabled = not m.shuffleEnabled
if m.shuffleEnabled
shuffleQueueItems()
return
end if
resetQueueItemOrder()
end sub
' Reset queue items back to original, unshuffled order
sub resetQueueItemOrder()
set(m.originalQueue)
end sub
' Return original, unshuffled queue
function getUnshuffledQueue()
return m.originalQueue
end function
' Save a copy of the original queue and randomize order of queue items
sub shuffleQueueItems()
' By calling getQueue 2 different ways, Roku avoids needing to do a deep copy
m.originalQueue = m.global.queueManager.callFunc("getQueue")
songIDArray = getQueue()
' Move the currently playing song to the front of the queue
temp = top()
songIDArray[0] = getCurrentItem()
songIDArray[getPosition()] = temp
for i = 1 to songIDArray.count() - 1
j = Rnd(songIDArray.count() - 1)
temp = songIDArray[i]
songIDArray[i] = songIDArray[j]
songIDArray[j] = temp
end for
set(songIDArray)
end sub
' Return the fitst item in the play queue
function top()
return getItemByIndex(0)
@ -115,7 +170,7 @@ end function
' Replace play queue with passed array
sub set(items)
setPosition(0)
clear()
m.queue = items
for each item in items
m.queueTypes.push(getItemType(item))

View File

@ -5,19 +5,23 @@
<function name="deleteAtIndex" />
<function name="getCount" />
<function name="getCurrentItem" />
<function name="getIsShuffled" />
<function name="getItemByIndex" />
<function name="getPosition" />
<function name="getQueue" />
<function name="getQueueTypes" />
<function name="getQueueUniqueTypes" />
<function name="getUnshuffledQueue" />
<function name="moveBack" />
<function name="moveForward" />
<function name="peek" />
<function name="playQueue" />
<function name="pop" />
<function name="push" />
<function name="resetShuffle" />
<function name="set" />
<function name="setPosition" />
<function name="toggleShuffle" />
<function name="top" />
</interface>
<script type="text/brightscript" uri="QueueManager.brs" />

View File

@ -10,7 +10,6 @@ sub init()
m.playlistTypeCount = m.global.queueManager.callFunc("getQueueUniqueTypes").count()
m.shuffleEnabled = false
m.buttonCount = m.buttons.getChildCount()
m.screenSaverTimeout = 300
@ -26,6 +25,8 @@ sub init()
loadButtons()
pageContentChanged()
setShuffleIconState()
setLoopButtonImage()
end sub
sub onScreensaverTimeoutLoaded()
@ -111,9 +112,10 @@ sub setupInfoNodes()
m.playPosition = m.top.findNode("playPosition")
m.bufferPosition = m.top.findNode("bufferPosition")
m.seekBar = m.top.findNode("seekBar")
m.numberofsongsField = m.top.findNode("numberofsongs")
m.shuffleIndicator = m.top.findNode("shuffleIndicator")
m.loopIndicator = m.top.findNode("loopIndicator")
m.positionTimestamp = m.top.findNode("positionTimestamp")
m.totalLengthTimestamp = m.top.findNode("totalLengthTimestamp")
end sub
sub bufferPositionChanged()
@ -156,6 +158,13 @@ sub audioPositionChanged()
m.playPositionAnimationWidth.keyValue = [m.playPosition.width, playPositionBarWidth]
m.playPositionAnimation.control = "start"
' Update displayed position timestamp
if isValid(m.global.audioPlayer.position)
m.positionTimestamp.text = secondsToHuman(m.global.audioPlayer.position)
else
m.positionTimestamp.text = "0:00"
end if
' Only fall into screensaver logic if the user has screensaver enabled in Roku settings
if m.screenSaverTimeout > 0
if m.di.TimeSinceLastKeypress() >= m.screenSaverTimeout - 2
@ -254,6 +263,11 @@ function previousClicked() as boolean
m.global.audioPlayer.control = "stop"
end if
' Reset loop mode due to manual user interaction
if m.global.audioPlayer.loopMode = "one"
resetLoopModeToDefault()
end if
m.global.queueManager.callFunc("moveBack")
pageContentChanged()
@ -261,6 +275,11 @@ function previousClicked() as boolean
return true
end function
sub resetLoopModeToDefault()
m.global.audioPlayer.loopMode = ""
setLoopButtonImage()
end sub
function loopClicked() as boolean
if m.global.audioPlayer.loopMode = ""
@ -290,6 +309,11 @@ end sub
function nextClicked() as boolean
if m.playlistTypeCount > 1 then return false
' Reset loop mode due to manual user interaction
if m.global.audioPlayer.loopMode = "one"
resetLoopModeToDefault()
end if
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
LoadNextSong()
end if
@ -298,10 +322,12 @@ function nextClicked() as boolean
end function
sub toggleShuffleEnabled()
m.shuffleEnabled = not m.shuffleEnabled
m.global.queueManager.callFunc("toggleShuffle")
end sub
function findCurrentSongIndex(songList) as integer
if not isValidAndNotEmpty(songList) then return 0
for i = 0 to songList.count() - 1
if songList[i].id = m.global.queueManager.callFunc("getCurrentItem").id
return i
@ -313,44 +339,36 @@ end function
function shuffleClicked() as boolean
currentSongIndex = findCurrentSongIndex(m.global.queueManager.callFunc("getUnshuffledQueue"))
toggleShuffleEnabled()
if not m.shuffleEnabled
if not m.global.queueManager.callFunc("getIsShuffled")
m.shuffleIndicator.opacity = ".4"
m.shuffleIndicator.uri = m.shuffleIndicator.uri.Replace("-on", "-off")
currentSongIndex = findCurrentSongIndex(m.originalSongList)
m.global.queueManager.callFunc("set", m.originalSongList)
m.global.queueManager.callFunc("setPosition", currentSongIndex)
setFieldTextValue("numberofsongs", "Track " + stri(m.global.queueManager.callFunc("getPosition") + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
setTrackNumberDisplay()
return true
end if
m.shuffleIndicator.opacity = "1"
m.shuffleIndicator.uri = m.shuffleIndicator.uri.Replace("-off", "-on")
m.originalSongList = m.global.queueManager.callFunc("getQueue")
songIDArray = m.global.queueManager.callFunc("getQueue")
' Move the currently playing song to the front of the queue
temp = m.global.queueManager.callFunc("top")
songIDArray[0] = m.global.queueManager.callFunc("getCurrentItem")
songIDArray[m.global.queueManager.callFunc("getPosition")] = temp
for i = 1 to songIDArray.count() - 1
j = Rnd(songIDArray.count() - 1)
temp = songIDArray[i]
songIDArray[i] = songIDArray[j]
songIDArray[j] = temp
end for
m.global.queueManager.callFunc("set", songIDArray)
setTrackNumberDisplay()
return true
end function
sub setShuffleIconState()
if m.global.queueManager.callFunc("getIsShuffled")
m.shuffleIndicator.opacity = "1"
m.shuffleIndicator.uri = m.shuffleIndicator.uri.Replace("-off", "-on")
end if
end sub
sub setTrackNumberDisplay()
setFieldTextValue("numberofsongs", "Track " + stri(m.global.queueManager.callFunc("getPosition") + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
end sub
sub LoadNextSong()
if m.global.audioPlayer.state = "playing"
m.global.audioPlayer.control = "stop"
@ -400,6 +418,9 @@ sub pageContentChanged()
setScreenTitle(currentItem)
setOnScreenTextValues(currentItem)
m.songDuration = currentItem.RunTimeTicks / 10000000.0
' Update displayed total audio length
m.totalLengthTimestamp.text = ticksToHuman(currentItem.RunTimeTicks)
end if
m.LoadAudioStreamTask.itemId = currentItem.id
@ -455,6 +476,9 @@ sub onMetaDataLoaded()
if isValid(data.json.RunTimeTicks)
m.songDuration = data.json.RunTimeTicks / 10000000.0
' Update displayed total audio length
m.totalLengthTimestamp.text = ticksToHuman(data.json.RunTimeTicks)
end if
end if
end sub
@ -492,14 +516,8 @@ end sub
' Populate on screen text variables
sub setOnScreenTextValues(json)
if isValid(json)
currentSongIndex = m.global.queueManager.callFunc("getPosition")
if m.shuffleEnabled
currentSongIndex = findCurrentSongIndex(m.originalSongList)
end if
if m.playlistTypeCount = 1
setFieldTextValue("numberofsongs", "Track " + stri(currentSongIndex + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
setTrackNumberDisplay()
end if
setFieldTextValue("artist", json.Artists[0])

View File

@ -4,6 +4,8 @@
<Poster id="backdrop" opacity=".5" loadDisplayMode="scaleToZoom" width="1920" height="1200" blendColor="#3f3f3f" />
<Poster id="shuffleIndicator" width="64" height="64" uri="pkg:/images/icons/shuffleIndicator-off.png" translation="[1150,775]" opacity="0" />
<Poster id="loopIndicator" width="64" height="64" uri="pkg:/images/icons/loopIndicator-off.png" translation="[700,775]" opacity="0" />
<Label id="positionTimestamp" width="100" height="25" horizAlign="right" font="font:SmallestSystemFont" translation="[590,825]" color="#999999" text="0:00" />
<Label id="totalLengthTimestamp" width="100" height="25" horizAlign="left" font="font:SmallestSystemFont" translation="[1230,825]" color="#999999" />
<LayoutGroup id="toplevel" layoutDirection="vert" horizAlignment="center" translation="[960,175]" itemSpacings="[40]">
<LayoutGroup id="main_group" layoutDirection="vert" horizAlignment="center" itemSpacings="[15]">
<Poster id="albumCover" width="500" height="500" />

View File

@ -23,7 +23,6 @@
<field id="videoId" type="string" />
<field id="mediaSourceId" type="string" />
<field id="audioIndex" type="integer" />
<field id="runTime" type="integer" />
</interface>
<script type="text/brightscript" uri="VideoPlayerView.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />

View File

@ -3,6 +3,7 @@ VSCode
BrightScript
sideload
Sideload
Reddit
DEVGUIDE
ing
hardcode

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.0" language="de_DE" sourcelanguage="en_US">
<defaultcodec>UTF-8</defaultcodec>
<context>
<defaultcodec>UTF-8</defaultcodec>
<context>
<name>default</name>
<message>
<source>192.168.1.100:8096 or https://example.com/jellyfin</source>
<translation>Standard: 192.168.1.100:8096 oder https://example.com/jellyfin</translation>
<translation>192.168.1.100:8096 oder https://example.com/jellyfin</translation>
</message>
<message>
<source>Cancel</source>
@ -180,8 +180,8 @@
<source>Server</source>
<translation>Server</translation>
</message>
</context>
<context>
</context>
<context>
<name></name>
<message>
<source>Sign Out</source>
@ -9961,5 +9961,58 @@
<source>Save Credentials?</source>
<translation>Zugangsdaten speichern?</translation>
</message>
</context>
<message>
<comment>Name or Title field of media item</comment>
<source>TITLE</source>
<translation>Name</translation>
</message>
<message>
<source>Sign Out</source>
<translation>Abmelden</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Löschen gespeichert</translation>
</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>Fehler beim Laden der Daten vom Server</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 trat ein Fehler auf</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Save Credentials?</source>
<translation>Zugangsdaten speichern?</translation>
</message>
<message>
<source>Error Retrieving Content</source>
<translation>Fehler beim Laden des Inhalts</translation>
<extracomment>Dialog title when unable to load Content from Server</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Lade Kanaldaten</translation>
</message>
<message>
<source>Error loading Channel Data</source>
<translation>Fehler beim Laden der Kanaldaten</translation>
</message>
<message>
<source>Unable to load Channel Data from the server</source>
<translation>Kanaldaten können nicht vom Server geladen werden</translation>
</message>
<message>
<source>Change Server</source>
<translation>Server ändern</translation>
</message>
</context>
</TS>

View File

@ -6,7 +6,7 @@
<name>default</name>
<message>
<source>192.168.1.100:8096 or https://example.com/jellyfin</source>
<translation>default192.168.1.100:8096 or https://example.com/jellyfin</translation>
<translation>192.168.1.100:8096 or https://example.com/jellyfin</translation>
</message>
<message>
<source>Cancel</source>
@ -3254,5 +3254,345 @@
<source>Save Credentials?</source>
<translation>Save Credentials?</translation>
</message>
<message>
<source>More Like This</source>
<translation>More Like This</translation>
</message>
<message>
<source>Additional Parts</source>
<translation>Additional Parts</translation>
<extracomment>Additional parts of a video</extracomment>
</message>
<message>
<source>Record</source>
<translation>Record</translation>
</message>
<message>
<source>Enter the server name or IP address</source>
<translation>Enter the server name or IP address</translation>
<extracomment>Title of KeyboardDialog when manually entering a server URL</extracomment>
</message>
<message>
<source>...or enter server URL manually:</source>
<translation>If no server is listed above, you may also enter the server URL manually:</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>Error Getting Playback Information</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>An error was encountered while playing this item. Server did not provide required transcoding data.</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>MPEG-4</source>
<translation>MPEG-4</translation>
<extracomment>Name of codec used in settings menu</extracomment>
</message>
<message>
<source>Cancel Series Recording</source>
<translation>Cancel Series Recording</translation>
</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>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>
</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>
<message>
<source>Media Grid</source>
<translation>Media Grid</translation>
<extracomment>UI -&gt; Media Grid section in user setting screen.</extracomment>
</message>
<message>
<source>Show item count in the library and index of selected item.</source>
<translation>Show item count in the library and index of selected item.</translation>
<extracomment>Description for option in Setting Screen</extracomment>
</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>AV1</source>
<translation>AV1</translation>
<extracomment>Name of a setting - should we try to direct play experimental av1 codec</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>(Dialog will close automatically)</source>
<translation>(Dialogue will close automatically)</translation>
</message>
<message>
<source>Blur Unwatched Episodes</source>
<translation>Blur Unwatched Episodes</translation>
<extracomment>Option Title in user setting screen</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>Cast &amp; Crew</source>
<translation>Cast &amp; Crew</translation>
</message>
<message>
<source>Special Features</source>
<translation>Special Features</translation>
</message>
<message>
<source>TV Shows</source>
<translation>TV Shows</translation>
</message>
<message>
<source>Close</source>
<translation>Close</translation>
</message>
<message>
<source>Unknown</source>
<translation>Unknown</translation>
<extracomment>Title for a cast member for which we have no information for</extracomment>
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Select an available Jellyfin server from your local network:</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>Cancel Recording</translation>
</message>
<message>
<source>If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.</source>
<translation>If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.</translation>
<extracomment>Settings Menu - Description for option</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>Set how many seconds before the end of an episode the Next Episode button should appear. Set to 0 to disable.</source>
<translation>Set how many seconds before the end of an episode the Next Episode button should appear. Set to 0 to disable.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Quick Connect</source>
<translation>Quick Connect</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>Media Grid options.</source>
<translation>Media Grid options.</translation>
</message>
<message>
<source>Studios</source>
<translation>Studios</translation>
</message>
<message>
<source>Hides tagline text on details pages.</source>
<translation>Hides tagline text on details pages.</translation>
</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 Splashscreen as Screensaver</source>
<translation>Use Splashscreen as Screensaver</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Record Series</source>
<translation>Record Series</translation>
</message>
<message>
<source>Version</source>
<translation>Version</translation>
</message>
<message>
<source>Playback</source>
<translation>Playback</translation>
<extracomment>Title for Playback section in user setting screen.</extracomment>
</message>
<message>
<source>User Interface</source>
<translation>User Interface</translation>
<extracomment>Title for User Interface section in user setting screen.</extracomment>
</message>
<message>
<source>Item Count</source>
<translation>Item Count</translation>
<extracomment>UI -&gt; Media Grid -&gt; Item Count in user setting screen.</extracomment>
</message>
<message>
<source>Set Watched</source>
<translation>Set Watched</translation>
<extracomment>Button Text - When pressed, marks item as Warched</extracomment>
</message>
<message>
<source>Here is your Quick Connect code:</source>
<translation>Here is your Quick Connect code:</translation>
</message>
<message>
<source>Networks</source>
<translation>Networks</translation>
</message>
<message>
<source>Codec Support</source>
<translation>Codec Support</translation>
<extracomment>Settings Menu - Title for settings group related to codec support</extracomment>
</message>
<message>
<source>MPEG-2</source>
<translation>MPEG-2</translation>
<extracomment>Name of codec used in settings menu</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>Description of a setting - should we try to direct play experimental av1 codec</extracomment>
</message>
<message>
<source>Disabled</source>
<translation>Disabled</translation>
</message>
<message>
<source>Set Favorite</source>
<translation>Set Favourite</translation>
<extracomment>Button Text - When pressed, sets item as Favorite</extracomment>
</message>
<message>
<source>Go to series</source>
<translation>Go to series</translation>
<extracomment>Continue Watching Popup Menu - Navigate to the Series 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>%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>Hide Taglines</source>
<translation>Hide Taglines</translation>
<extracomment>Option Title in user setting screen</extracomment>
</message>
<message>
<source>Skip Details for Single Seasons</source>
<translation>Skip Details for Single Seasons</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</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>Shows</source>
<translation>Shows</translation>
</message>
<message>
<source>Enabled</source>
<translation>Enabled</translation>
</message>
<message>
<source>Save Credentials?</source>
<translation>Save Credentials?</translation>
</message>
<message>
<source>Age</source>
<translation>Age</translation>
</message>
<message>
<source>Additional Parts</source>
<translation>Additional Parts</translation>
<extracomment>Additional parts of a video</extracomment>
</message>
<message>
<source>On Now</source>
<translation>On Now</translation>
</message>
<message>
<source>Cast &amp; Crew</source>
<translation>Cast &amp; Crew</translation>
</message>
<message>
<source>More Like This</source>
<translation>More Like This</translation>
</message>
<message>
<source>Died</source>
<translation>Died</translation>
</message>
<message>
<source>Delete Saved</source>
<translation>Delete Saved</translation>
</message>
<message>
<source>Born</source>
<translation>Born</translation>
</message>
<message>
<source>Special Features</source>
<translation>Special Features</translation>
</message>
<message>
<source>Press &apos;OK&apos; to Close</source>
<translation>Press &apos;OK&apos; to Close</translation>
</message>
<message>
<source>Set how many seconds before the end of an episode the Next Episode button should appear. Set to 0 to disable.</source>
<translation>Set how many seconds before the end of an episode the Next Episode button should appear. Set to 0 to disable.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
</context>
</TS>

View File

@ -675,13 +675,13 @@
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.</source>
<translation>If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.</translation>
<source>Go directly to the episode list if a TV series has only one season.</source>
<translation>Go directly to the episode list if a TV series has only one season.</translation>
<extracomment>Settings Menu - Description for option</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>
<source>Blur images of unwatched episodes.</source>
<translation>Blur images of unwatched episodes.</translation>
</message>
<message>
<source>Design Elements</source>
@ -708,8 +708,8 @@
<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>Cinema Mode brings the theater experience straight to your living room with the ability to play custom intros before the main feature.</translation>
<source>Bring the theater experience straight to your living room with the ability to play custom intros before the main feature.</source>
<translation>Bring the theater 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>
@ -718,8 +718,8 @@
<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>Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.</translation>
<source>Hide all clocks in Jellyfin. Jellyfin will need to be closed and reopened for changes to take effect.</source>
<translation>Hide all clocks in Jellyfin. Jellyfin will need to be closed and reopened for changes to take effect.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
@ -1007,36 +1007,36 @@
<translation>Disable Community Rating for Episodes</translation>
</message>
<message>
<source>If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.</source>
<translation>If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.</translation>
<source>Hide the star and community rating for episodes of a TV show. This is to prevent spoilers of an upcoming good/bad episode.</source>
<translation>Hide the star and community rating for episodes of a TV show. This is to prevent spoilers of an upcoming good/bad episode.</translation>
</message>
<message>
<source>Configure the maximum playback bitrate.</source>
<translation>Configure the maximum playback bitrate.</translation>
</message>
<message>
<source>Biographical information for this person is not currently available.</source>
<translation>Biographical information for this person is not currently available.</translation>
</message>
<message>
<source>Playback Bitrate Limits</source>
<translation>Playback Bitrate Limits</translation>
<source>Enable Limit</source>
<translation>Enable Limit</translation>
</message>
<message>
<source>Set limits for how high playback bitrates are allowed to be.</source>
<translation>Set limits for how high playback bitrates are allowed to be.</translation>
<source>Enable or disable the 'Maximum Bitrate' setting.</source>
<translation>Enable or disable the 'Maximum Bitrate' setting.</translation>
</message>
<message>
<source>Limits Enabled</source>
<translation>Limits Enabled</translation>
<source>Bitrate Limit</source>
<translation>Bitrate Limit</translation>
</message>
<message>
<source>If enabled, playback bitrates will be limited based on the 'Playback Bitrate Limit' setting.</source>
<translation>If enabled, playback bitrates will be limited based on the 'Playback Bitrate Limit' setting.</translation>
<source>Maximum Bitrate</source>
<translation>Maximum Bitrate</translation>
</message>
<message>
<source>Playback Bitrate Limit</source>
<translation>Playback Bitrate Limit</translation>
</message>
<message>
<source>Max bitrate (Mbps) allowed if limits are enabled. Set to 0 to use Roku's specifications.</source>
<translation>Max bitrate (Mbps) allowed if limits are enabled. Set to 0 to use Roku's specifications.</translation>
<source>Set the maximum bitrate in Mbps. Set to 0 to use Roku's specifications. This setting must be enabled to take effect.</source>
<translation>Set the maximum bitrate in Mbps. Set to 0 to use Roku's specifications. This setting must be enabled to take effect.</translation>
</message>
<message>
<source>Libraries</source>

View File

@ -3,7 +3,7 @@
title=Jellyfin
major_version=1
minor_version=6
build_version=4
build_version=5
### Main Menu Icons / Channel Poster Artwork
@ -21,4 +21,6 @@ splash_min_time=1500
ui_resolutions=fhd
confirm_partner_button=1
supports_input_launch=1

269
package-lock.json generated
View File

@ -18,7 +18,7 @@
},
"devDependencies": {
"@rokucommunity/bslint": "0.8.2",
"brighterscript": "0.62.0",
"brighterscript": "0.64.0",
"jshint": "^2.13.6",
"markdownlint-cli2": "0.6.0",
"ropm": "0.10.12",
@ -448,9 +448,10 @@
}
},
"node_modules/brighterscript": {
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.62.0.tgz",
"integrity": "sha512-aFDlceBnU5oQI+/pb+QMwkIGylT880KteRxDF1wS5F2TMK31rRn3Ifh1WcQrr5gEKnPebpCL0ZYvWNSeVKVqmg==",
"version": "0.64.0",
"resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.64.0.tgz",
"integrity": "sha512-uLxQlrUcsW1QS9I8xerevDYgE/Tozwa+O3PSUnPcdCFUytE8hIDTtyroQG+WApKaUeHpGI0HlEzr7pWyEqXRAA==",
"dev": true,
"dependencies": {
"@rokucommunity/bslib": "^0.1.1",
"@xml-tools/parser": "^1.0.7",
@ -503,6 +504,81 @@
"bsfmt": "dist/cli.js"
}
},
"node_modules/brighterscript-formatter/node_modules/brighterscript": {
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.62.0.tgz",
"integrity": "sha512-aFDlceBnU5oQI+/pb+QMwkIGylT880KteRxDF1wS5F2TMK31rRn3Ifh1WcQrr5gEKnPebpCL0ZYvWNSeVKVqmg==",
"dependencies": {
"@rokucommunity/bslib": "^0.1.1",
"@xml-tools/parser": "^1.0.7",
"array-flat-polyfill": "^1.0.1",
"chalk": "^2.4.2",
"chevrotain": "^7.0.1",
"chokidar": "^3.5.1",
"clear": "^0.1.0",
"cross-platform-clear-console": "^2.3.0",
"debounce-promise": "^3.1.0",
"eventemitter3": "^4.0.0",
"fast-glob": "^3.2.11",
"file-url": "^3.0.0",
"fs-extra": "^8.0.0",
"jsonc-parser": "^2.3.0",
"long": "^3.2.0",
"luxon": "^2.5.2",
"minimatch": "^3.0.4",
"moment": "^2.23.0",
"p-settle": "^2.1.0",
"parse-ms": "^2.1.0",
"require-relative": "^0.8.7",
"roku-deploy": "^3.10.0",
"serialize-error": "^7.0.1",
"source-map": "^0.7.4",
"vscode-languageserver": "7.0.0",
"vscode-languageserver-protocol": "3.16.0",
"vscode-languageserver-textdocument": "^1.0.1",
"vscode-uri": "^2.1.1",
"xml2js": "^0.4.19",
"yargs": "^16.2.0"
},
"bin": {
"bsc": "dist/cli.js"
}
},
"node_modules/brighterscript-formatter/node_modules/brighterscript/node_modules/jsonc-parser": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz",
"integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="
},
"node_modules/brighterscript-formatter/node_modules/brighterscript/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/brighterscript-formatter/node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/brighterscript-formatter/node_modules/glob-all": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/glob-all/-/glob-all-3.3.1.tgz",
@ -585,6 +661,7 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
@ -598,6 +675,7 @@
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
@ -3991,6 +4069,61 @@
"chevrotain": "7.1.1"
}
},
"node_modules/ropm/node_modules/brighterscript": {
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.62.0.tgz",
"integrity": "sha512-aFDlceBnU5oQI+/pb+QMwkIGylT880KteRxDF1wS5F2TMK31rRn3Ifh1WcQrr5gEKnPebpCL0ZYvWNSeVKVqmg==",
"dev": true,
"dependencies": {
"@rokucommunity/bslib": "^0.1.1",
"@xml-tools/parser": "^1.0.7",
"array-flat-polyfill": "^1.0.1",
"chalk": "^2.4.2",
"chevrotain": "^7.0.1",
"chokidar": "^3.5.1",
"clear": "^0.1.0",
"cross-platform-clear-console": "^2.3.0",
"debounce-promise": "^3.1.0",
"eventemitter3": "^4.0.0",
"fast-glob": "^3.2.11",
"file-url": "^3.0.0",
"fs-extra": "^8.0.0",
"jsonc-parser": "^2.3.0",
"long": "^3.2.0",
"luxon": "^2.5.2",
"minimatch": "^3.0.4",
"moment": "^2.23.0",
"p-settle": "^2.1.0",
"parse-ms": "^2.1.0",
"require-relative": "^0.8.7",
"roku-deploy": "^3.10.0",
"serialize-error": "^7.0.1",
"source-map": "^0.7.4",
"vscode-languageserver": "7.0.0",
"vscode-languageserver-protocol": "3.16.0",
"vscode-languageserver-textdocument": "^1.0.1",
"vscode-uri": "^2.1.1",
"xml2js": "^0.4.19",
"yargs": "^16.2.0"
},
"bin": {
"bsc": "dist/cli.js"
}
},
"node_modules/ropm/node_modules/brighterscript/node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/ropm/node_modules/chevrotain": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.1.tgz",
@ -5353,9 +5486,10 @@
}
},
"brighterscript": {
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.62.0.tgz",
"integrity": "sha512-aFDlceBnU5oQI+/pb+QMwkIGylT880KteRxDF1wS5F2TMK31rRn3Ifh1WcQrr5gEKnPebpCL0ZYvWNSeVKVqmg==",
"version": "0.64.0",
"resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.64.0.tgz",
"integrity": "sha512-uLxQlrUcsW1QS9I8xerevDYgE/Tozwa+O3PSUnPcdCFUytE8hIDTtyroQG+WApKaUeHpGI0HlEzr7pWyEqXRAA==",
"dev": true,
"requires": {
"@rokucommunity/bslib": "^0.1.1",
"@xml-tools/parser": "^1.0.7",
@ -5393,6 +5527,7 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
@ -5403,6 +5538,7 @@
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
@ -5427,6 +5563,74 @@
"yargs": "^17.2.1"
},
"dependencies": {
"brighterscript": {
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.62.0.tgz",
"integrity": "sha512-aFDlceBnU5oQI+/pb+QMwkIGylT880KteRxDF1wS5F2TMK31rRn3Ifh1WcQrr5gEKnPebpCL0ZYvWNSeVKVqmg==",
"requires": {
"@rokucommunity/bslib": "^0.1.1",
"@xml-tools/parser": "^1.0.7",
"array-flat-polyfill": "^1.0.1",
"chalk": "^2.4.2",
"chevrotain": "^7.0.1",
"chokidar": "^3.5.1",
"clear": "^0.1.0",
"cross-platform-clear-console": "^2.3.0",
"debounce-promise": "^3.1.0",
"eventemitter3": "^4.0.0",
"fast-glob": "^3.2.11",
"file-url": "^3.0.0",
"fs-extra": "^8.0.0",
"jsonc-parser": "^2.3.0",
"long": "^3.2.0",
"luxon": "^2.5.2",
"minimatch": "^3.0.4",
"moment": "^2.23.0",
"p-settle": "^2.1.0",
"parse-ms": "^2.1.0",
"require-relative": "^0.8.7",
"roku-deploy": "^3.10.0",
"serialize-error": "^7.0.1",
"source-map": "^0.7.4",
"vscode-languageserver": "7.0.0",
"vscode-languageserver-protocol": "3.16.0",
"vscode-languageserver-textdocument": "^1.0.1",
"vscode-uri": "^2.1.1",
"xml2js": "^0.4.19",
"yargs": "^16.2.0"
},
"dependencies": {
"jsonc-parser": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz",
"integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
}
}
}
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"glob-all": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/glob-all/-/glob-all-3.3.1.tgz",
@ -7965,6 +8169,57 @@
"chevrotain": "7.1.1"
}
},
"brighterscript": {
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.62.0.tgz",
"integrity": "sha512-aFDlceBnU5oQI+/pb+QMwkIGylT880KteRxDF1wS5F2TMK31rRn3Ifh1WcQrr5gEKnPebpCL0ZYvWNSeVKVqmg==",
"dev": true,
"requires": {
"@rokucommunity/bslib": "^0.1.1",
"@xml-tools/parser": "^1.0.7",
"array-flat-polyfill": "^1.0.1",
"chalk": "^2.4.2",
"chevrotain": "^7.0.1",
"chokidar": "^3.5.1",
"clear": "^0.1.0",
"cross-platform-clear-console": "^2.3.0",
"debounce-promise": "^3.1.0",
"eventemitter3": "^4.0.0",
"fast-glob": "^3.2.11",
"file-url": "^3.0.0",
"fs-extra": "^8.0.0",
"jsonc-parser": "^2.3.0",
"long": "^3.2.0",
"luxon": "^2.5.2",
"minimatch": "^3.0.4",
"moment": "^2.23.0",
"p-settle": "^2.1.0",
"parse-ms": "^2.1.0",
"require-relative": "^0.8.7",
"roku-deploy": "^3.10.0",
"serialize-error": "^7.0.1",
"source-map": "^0.7.4",
"vscode-languageserver": "7.0.0",
"vscode-languageserver-protocol": "3.16.0",
"vscode-languageserver-textdocument": "^1.0.1",
"vscode-uri": "^2.1.1",
"xml2js": "^0.4.19",
"yargs": "^16.2.0"
},
"dependencies": {
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
}
}
},
"chevrotain": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.1.tgz",

View File

@ -1,11 +1,11 @@
{
"name": "jellyfin-roku",
"version": "1.6.4",
"version": "1.6.5",
"description": "Roku app for Jellyfin media server",
"main": "index.js",
"devDependencies": {
"@rokucommunity/bslint": "0.8.2",
"brighterscript": "0.62.0",
"brighterscript": "0.64.0",
"ropm": "0.10.12",
"jshint": "^2.13.6",
"markdownlint-cli2": "0.6.0",

View File

@ -3,9 +3,29 @@
"title": "Playback",
"description": "Settings relating to playback and supported codec and media types.",
"children": [
{
"title": "Bitrate Limit",
"description": "Configure the maximum playback bitrate.",
"children": [
{
"title": "Enable Limit",
"description": "Enable or disable the 'Maximum Bitrate' setting.",
"settingName": "playback.bitrate.maxlimited",
"type": "bool",
"default": "true"
},
{
"title": "Maximum Bitrate",
"description": "Set the maximum bitrate in Mbps. Set to 0 to use Roku's specifications. This setting must be enabled to take effect.",
"settingName": "playback.bitrate.limit",
"type": "integer",
"default": "0"
}
]
},
{
"title": "Codec Support",
"description": "Enable or disable Direct Play support for certain codecs",
"description": "Enable or disable Direct Play support for certain codecs.",
"children": [
{
"title": "AV1",
@ -30,26 +50,6 @@
}
]
},
{
"title": "Playback Bitrate Limits",
"description": "Set limits for how high playback bitrates are allowed to be.",
"children": [
{
"title": "Limits Enabled",
"description": "If enabled, playback bitrates will be limited based on the 'Playback Bitrate Limit' setting.",
"settingName": "playback.bitrate.maxlimited",
"type": "bool",
"default": "true"
},
{
"title": "Playback Bitrate Limit",
"description": "Max bitrate (Mbps) allowed if limits are enabled. Set to 0 to use Roku's specifications.",
"settingName": "playback.bitrate.limit",
"type": "integer",
"default": "0"
}
]
},
{
"title": "Profile Level Support",
"description": "Attempt Direct Play of potentially unsupported profile levels",
@ -72,7 +72,7 @@
},
{
"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": "Bring the theater experience straight to your living room with the ability to play custom intros before the main feature.",
"settingName": "playback.cinemamode",
"type": "bool",
"default": "false"
@ -110,7 +110,7 @@
"children": [
{
"title": "Hide Clock",
"description": "Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.",
"description": "Hide all clocks in Jellyfin. Jellyfin will need to be closed and reopened for changes to take effect.",
"settingName": "ui.design.hideclock",
"type": "bool",
"default": "false"
@ -232,14 +232,14 @@
"children": [
{
"title": "Blur Unwatched Episodes",
"description": "If enabled, images of unwatched episodes will be blurred.",
"description": "Blur images of unwatched episodes.",
"settingName": "ui.tvshows.blurunwatched",
"type": "bool",
"default": "false"
},
{
"title": "Disable Community Rating for Episodes",
"description": "If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.",
"description": "Hide the star and community rating for episodes of a TV show. This is to prevent spoilers of an upcoming good/bad episode.",
"settingName": "ui.tvshows.disableCommunityRating",
"type": "bool",
"default": "false"

View File

@ -30,7 +30,7 @@ sub Main (args as dynamic) as void
m.port = CreateObject("roMessagePort")
m.screen.setMessagePort(m.port)
m.scene = m.screen.CreateScene("JFScene")
m.screen.show()
m.screen.show() ' vscode_rale_tracker_entry
' Set any initial Global Variables
m.global = m.screen.getGlobalNode()
@ -60,15 +60,21 @@ sub Main (args as dynamic) as void
m.scene.observeField("exit", m.port)
' Downloads and stores a fallback font to tmp:/
if parseJSON(APIRequest("/System/Configuration/encoding").GetToString())["EnableFallbackFont"] = true
configEncoding = api_API().system.getconfigurationbyname("encoding")
if isValid(configEncoding) and isValid(configEncoding.EnableFallbackFont)
if configEncoding.EnableFallbackFont
re = CreateObject("roRegex", "Name.:.(.*?).,.Size", "s")
filename = APIRequest("FallbackFont/Fonts").GetToString()
if isValid(filename)
filename = re.match(filename)
if filename.count() > 0
if isValid(filename) and filename.count() > 0
filename = filename[1]
APIRequest("FallbackFont/Fonts/" + filename).gettofile("tmp:/font")
end if
end if
end if
end if
' Only show the Whats New popup the first time a user runs a new client version.
if appInfo.GetVersion() <> get_setting("LastRunVersion")
@ -239,6 +245,7 @@ sub Main (args as dynamic) as void
group = CreatePlaylistView(selectedItem.json)
else if selectedItem.type = "Audio"
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("push", selectedItem.json)
m.global.queueManager.callFunc("playQueue")
else
@ -280,6 +287,7 @@ sub Main (args as dynamic) as void
screenContent = msg.getRoSGNode()
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("push", screenContent.albumData.items[selectedIndex])
m.global.queueManager.callFunc("playQueue")
else if isNodeEvent(msg, "playItem")
@ -288,6 +296,7 @@ sub Main (args as dynamic) as void
screenContent = msg.getRoSGNode()
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("push", screenContent.albumData.items[selectedIndex])
m.global.queueManager.callFunc("playQueue")
else if isNodeEvent(msg, "playAllSelected")
@ -297,6 +306,7 @@ sub Main (args as dynamic) as void
m.spinner.visible = true
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("set", screenContent.albumData.items)
m.global.queueManager.callFunc("playQueue")
else if isNodeEvent(msg, "playArtistSelected")
@ -304,6 +314,7 @@ sub Main (args as dynamic) as void
screenContent = msg.getRoSGNode()
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("set", CreateArtistMix(screenContent.pageContent.id).Items)
m.global.queueManager.callFunc("playQueue")
@ -323,6 +334,7 @@ sub Main (args as dynamic) as void
if isValid(screenContent.albumData.items)
if screenContent.albumData.items.count() > 0
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("set", CreateInstantMix(screenContent.albumData.items[0].id).Items)
m.global.queueManager.callFunc("playQueue")
@ -334,6 +346,7 @@ sub Main (args as dynamic) as void
if not viewHandled
' Create instant mix based on selected artist
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("set", CreateInstantMix(screenContent.pageContent.id).Items)
m.global.queueManager.callFunc("playQueue")
end if
@ -381,6 +394,7 @@ sub Main (args as dynamic) as void
group = CreateAlbumView(node.json)
else if node.type = "Audio"
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("push", node.json)
m.global.queueManager.callFunc("playQueue")
else if node.type = "Person"
@ -395,6 +409,7 @@ sub Main (args as dynamic) as void
selectedIndex = msg.getData()
screenContent = msg.getRoSGNode()
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("push", screenContent.albumData.items[node.id])
m.global.queueManager.callFunc("playQueue")
else

View File

@ -47,9 +47,6 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
end if
if m.videotype = "Episode" or m.videotype = "Series"
if isValid(meta.json) and isValid(meta.json.RunTimeTicks)
video.runTime = (meta.json.RunTimeTicks / 10000000.0)
end if
video.content.contenttype = "episode"
end if

View File

@ -1,146 +1,50 @@
[
{
"description": "Feature: Phase 1 CJK subtitle support - external files only",
"author": "jkim2492"
"description": "Bug Fix: Rows on home view not refreshing when data has changed",
"author": "1hitsong"
},
{
"description": "Bug Fix: Fix multiple client crashes identified by crashlogs",
"description": "Bug Fix: Next up section does not populate correctly after watching a TV episode",
"author": "1hitsong"
},
{
"description": "Bug Fix: Next episode button does not display consistently",
"author": "1hitsong"
},
{
"description": "Bug Fix: Crash when fallback font API call fails",
"author": "1hitsong"
},
{
"description": "Core: Fix Jellyfin links in readme",
"author": "cewert"
},
{
"description": "Feature: Add TV series & season shuffle",
"description": "Bug Fix: Music shuffle not working",
"author": "1hitsong"
},
{
"description": "Updated View: Show \"Actor\" when an actor has no role",
"description": "Updated View: Add current playback time and song length to the sides of the audio progress bar",
"author": "1hitsong"
},
{
"description": "Bug Fix: Turn off loop mode when user manually changes song",
"author": "1hitsong"
},
{
"description": "Core: Enable confirm partner button and setup RALE",
"author": "cewert"
},
{
"description": "Feature: Phase 1 playlist support",
"author": "1hitsong"
},
{
"description": "Fix Typo: trancoding -> transcoding",
"author": "RussianCow"
},
{
"description": "Feature: Create global audio player",
"author": "1hitsong"
},
{
"description": "Core: Add user policy to check if canDelete",
"author": "candry7731"
},
{
"description": "Bug Fix: Only show next episode button if \"Play next episode automatically\" setting is enabled in web client",
"description": "Bug Fix: Clock not displaying on login view",
"author": "cewert"
},
{
"description": "New Setting: Disable unwatched episode count",
"author": "1hitsong"
"description": "Core: Add build badge and rework readme content",
"author": "cewert"
},
{
"description": "New Setting: Next episode button time",
"author": "candry7731"
},
{
"description": "New View: New persondetails view",
"description": "Core: Adjust settings to follow latest guidelines",
"author": "sevenrats"
},
{
"description": "Updated View: Make title scrolling consistent in extras slider",
"author": "sevenrats"
},
{
"description": "Feature: Make pressing the options button close settings menu",
"author": "sevenrats"
},
{
"description": "Bug Fix: Graceful episode playback failure",
"author": "sevenrats"
},
{
"description": "Bug Fix: Fix default view setting for movie genres",
"author": "1hitsong"
},
{
"description": "Core: Update CI ubuntu version and node version",
"author": "sevenrats"
},
{
"description": "New Setting: Custom max video bitrate",
"author": "jimdogx"
},
{
"description": "Updated View: Updated \"OnNow\" home row to default to channel images if program images are not availible",
"author": "candry7731"
},
{
"description": "Updated View: Improve settings menu, implement title hover and hide in missing locations",
"author": "sevenrats"
},
{
"description": "Updated View: Make home view load faster",
"author": "1hitsong"
},
{
"description": "Bug Fix: Fix option menu focus if opened while library still loading",
"author": "ApexArray"
},
{
"description": "Updated View: Add Genres, Parental Ratings, and Years as movie filters",
"author": "1hitsong"
},
{
"description": "Bug Fix: Revert change that removed image cache busting",
"author": "1hitsong"
},
{
"description": "Updated View: Fix distorted TV episode posters. Add client-side progress bar and played indicator.",
"author": "ApexArray"
},
{
"description": "Updated View: Improve quality of album art on now playing view",
"author": "1hitsong"
},
{
"description": "Updated View: Loading spinner, Progress Dialog and movie details button animate",
"author": "candry7731"
},
{
"description": "Core: Add settings guidelines to Dev Guide",
"author": "sevenrats"
},
{
"description": "Core: Add workflow to validate language translation files",
"author": "cewert"
},
{
"description": "Core: Remove optional chaining operators from code",
"author": "cewert"
},
{
"description": "Core: Make CI throw error for duplicate translation entries",
"author": "cewert"
},
{
"description": "Core: Fix en_US translation file",
"author": "cewert"
},
{
"description": "Core: Add a production build workflow",
"author": "cewert"
},
{
"description": "Core: Add json and markdown to lint workflow + add automation workflow",
"author": "cewert"
},
{
"description": "Core: Make workflows use latest LTS release + cache npm",
"author": "cewert"
},
{
"description": "Core: Install & Configure code unit test suite",
"author": "1hitsong"
}
]

View File

@ -41,6 +41,18 @@ function ticksToHuman(ticks as longinteger) as string
return r
end function
function secondsToHuman(totalSeconds as integer) as string
hours = stri(int(totalSeconds / 3600)).trim()
minutes = stri(int((totalSeconds - (val(hours) * 3600)) / 60)).trim()
seconds = stri(totalSeconds - (val(hours) * 3600) - (val(minutes) * 60)).trim()
if val(hours) > 0 and val(minutes) < 10 then minutes = "0" + minutes
if val(seconds) < 10 then seconds = "0" + seconds
r = ""
if val(hours) > 0 then r = hours + ":"
r = r + minutes + ":" + seconds
return r
end function
' Format time as 12 or 24 hour format based on system clock setting
function formatTime(time) as string
hours = time.getHours()