Merge remote-tracking branch 'upstream/master' into cache-global

This commit is contained in:
Charles Ewert 2024-10-17 12:06:31 -04:00
commit 79d671d4f4
48 changed files with 1239 additions and 248 deletions

View File

@ -10,7 +10,7 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
with:
node-version: "lts/*"
@ -21,7 +21,7 @@ jobs:
run: npm run ropm
- name: Build app
run: npm run build
- uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
with:
name: Jellyfin-Roku-dev-${{ github.sha }}
path: ${{ github.workspace }}/build/staging

View File

@ -13,7 +13,7 @@ jobs:
# Give the default GITHUB_TOKEN write permission to commit and push the changed files back to the repository.
contents: write
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.JF_BOT_TOKEN }}

View File

@ -12,7 +12,7 @@ jobs:
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'release-prep') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
with:
node-version: "lts/*"
@ -23,7 +23,7 @@ jobs:
run: npm run ropm
- name: Build app for production
run: npm run build-prod
- uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
with:
name: Jellyfin-Roku-v${{ env.newManVersion }}-${{ github.sha }}
path: ${{ github.workspace }}/build/staging

View File

@ -26,7 +26,7 @@ jobs:
steps:
# Setup
- name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Install required packages
uses: awalsh128/cache-apt-pkgs-action@latest
with:
@ -50,7 +50,7 @@ jobs:
run: echo "targetBranch=${{ env.bugfixBranch }}" >> $GITHUB_ENV
- name: Checkout bugfix branch
if: github.event.inputs.targetBranch == 'bugfix'
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
ref: ${{ env.targetBranch }}
# Save old version again if needed
@ -101,7 +101,7 @@ jobs:
steps:
# Setup
- name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Install jq to update json
uses: awalsh128/cache-apt-pkgs-action@latest
with:
@ -125,7 +125,7 @@ jobs:
run: echo "targetBranch=${{ env.bugfixBranch }}" >> $GITHUB_ENV
- name: Checkout bugfix branch
if: github.event.inputs.targetBranch == 'bugfix'
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
ref: ${{ env.targetBranch }}
# Calculate new version
@ -169,7 +169,7 @@ jobs:
steps:
# Setup
- name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Install jq to update json
uses: awalsh128/cache-apt-pkgs-action@latest
with:
@ -193,7 +193,7 @@ jobs:
run: echo "targetBranch=${{ env.bugfixBranch }}" >> $GITHUB_ENV
- name: Checkout bugfix branch
if: github.event.inputs.targetBranch == 'bugfix'
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
ref: ${{ env.targetBranch }}
# Calculate new version

View File

@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Setup Pages
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
- name: Upload artifact

View File

@ -12,7 +12,7 @@ jobs:
if: github.repository == 'jellyfin/jellyfin-roku' && github.event_name != 'pull_request' || github.repository == 'jellyfin/jellyfin-roku' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
with:
node-version: "lts/*"

View File

@ -3,7 +3,7 @@
# If you want to get_images, you'll also need convert from ImageMagick
##########################################################################
VERSION := 2.1.5
VERSION := 2.1.8
## usage

View File

@ -4,7 +4,7 @@ import "pkg:/source/utils/config.bs"
sub init()
m.itemPoster = m.top.findNode("itemPoster")
m.posterText = m.top.findNode("posterText")
m.title = m.top.findNode("title")
initTitle()
m.posterText.font.size = 30
m.title.font.size = 25
m.backdrop = m.top.findNode("backdrop")
@ -23,6 +23,10 @@ sub init()
end if
end sub
sub initTitle()
m.title = m.top.findNode("title")
end sub
sub itemContentChanged()
m.backdrop.blendColor = "#101010"
@ -54,6 +58,8 @@ sub itemContentChanged()
end sub
sub focusChanged()
if not isValid(m.title) then initTitle()
if m.top.itemHasFocus = true
m.title.repeatCount = -1
else

View File

@ -6,6 +6,7 @@ import "pkg:/source/utils/config.bs"
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/userauth.bs"
import "pkg:/source/utils/deviceCapabilities.bs"
import "pkg:/source/utils/session.bs"
enum SubtitleSelection
notset = -2
@ -73,8 +74,6 @@ end function
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean)
meta = ItemMetaData(video.id)
subtitle_idx = m.top.selectedSubtitleIndex
if not isValid(meta)
video.errorMsg = "Error loading metadata"
video.content = invalid
@ -85,6 +84,20 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
userSession = m.global.session.user
userSettings = userSession.settings
session.video.Update(meta)
if isValid(meta.json.MediaSources[0].RunTimeTicks)
if meta.json.MediaSources[0].RunTimeTicks = 0
video.length = 0
else
video.length = meta.json.MediaSources[0].RunTimeTicks / 10000000
end if
end if
if isValid(meta.json.MediaSources[0]) and isValid(meta.json.MediaSources[0].MediaStreams[0])
video.MaxVideoDecodeResolution = [meta.json.MediaSources[0].MediaStreams[0].Width, meta.json.MediaSources[0].MediaStreams[0].Height]
end if
subtitle_idx = m.top.selectedSubtitleIndex
videotype = LCase(meta.type)
' Check for any Live TV streams or Recordings coming from other places other than the TV Guide
@ -202,12 +215,10 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
}
end if
' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay
fully_external = false
' For h264/hevc video, Roku spec states that it supports specfic encoding levels
' The device can decode content with a Higher Encoding level but may play it back with certain
' artifacts. If the user preference is set, and the only reason the server says we need to
@ -357,11 +368,15 @@ sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
if protocol <> "file"
uri = parseUrl(m.playbackInfo.MediaSources[0].Path)
if isLocalhost(uri[2])
if not isValidAndNotEmpty(uri) then return
if isValid(uri[2]) and isLocalhost(uri[2])
' if the domain of the URI is local to the server,
' create a new URI by appending the received path to the server URL
' later we will substitute the users provided URL for this case
video.content.url = buildURL(uri[4])
if isValid(uri[4])
video.content.url = buildURL(uri[4])
end if
else
fully_external = true
video.content.url = m.playbackInfo.MediaSources[0].Path

View File

@ -18,10 +18,8 @@ sub init()
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
end if
m.gridTitles = m.global.session.user.settings["itemgrid.gridTitles"]
m.posterText.visible = false
m.postTextBackground.visible = false
end sub
sub itemContentChanged()

View File

@ -1,7 +1,9 @@
import "pkg:/source/utils/config.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/roku_modules/log/LogMixin.brs"
sub init()
m.log = log.Logger("captionTask")
m.top.observeField("url", "fetchCaption")
m.top.currentCaption = []
m.top.currentPos = 0
@ -41,17 +43,26 @@ sub setFont()
end sub
sub fetchCaption()
m.log.debug("start fetchCaption()")
m.captionTimer.control = "stop"
re = CreateObject("roRegex", "(http.*?\.vtt)", "s")
url = re.match(m.top.url)[0]
if url <> invalid
port = createObject("roMessagePort")
m.reader.setUrl(url)
text = m.reader.GetToString()
m.captionList = parseVTT(text)
m.captionTimer.control = "start"
m.reader.setMessagePort(port)
if m.reader.AsyncGetToString()
msg = port.waitMessage(0)
if type(msg) = "roUrlEvent"
m.captionList = parseVTT(msg.GetString())
m.captionTimer.control = "start"
end if
end if
else
m.captionTimer.control = "stop"
end if
m.log.debug("end fetchCaption()", url)
end sub
function newlabel(txt)

View File

@ -4,9 +4,12 @@ import "pkg:/source/utils/config.bs"
sub setFields()
json = m.top.json
m.top.Type = "Person"
if json = invalid then return
m.top.id = json.id
m.top.favorite = json.UserData.isFavorite
m.top.Type = "Person"
setPoster()
end sub

View File

@ -10,12 +10,12 @@ sub init()
initItemPoster()
m.itemProgress = m.top.findNode("progress")
m.itemProgressBackground = m.top.findNode("progressBackground")
m.itemIcon = m.top.findNode("itemIcon")
initItemIcon()
initItemTextExtra()
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
m.unplayedCount = m.top.findNode("unplayedCount")
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
m.playedIndicator = m.top.findNode("playedIndicator")
initPlayedIndicator()
m.showProgressBarAnimation = m.top.findNode("showProgressBar")
m.showProgressBarField = m.top.findNode("showProgressBarField")
@ -50,6 +50,14 @@ sub initBackdrop()
m.backdrop = m.top.findNode("backdrop")
end sub
sub initItemIcon()
m.itemIcon = m.top.findNode("itemIcon")
end sub
sub initPlayedIndicator()
m.playedIndicator = m.top.findNode("playedIndicator")
end sub
sub itemContentChanged()
if isValid(m.unplayedCount) then m.unplayedCount.visible = false
itemData = m.top.itemContent
@ -63,6 +71,8 @@ sub itemContentChanged()
if not isValid(m.itemText) then initItemText()
if not isValid(m.itemTextExtra) then initItemTextExtra()
if not isValid(m.backdrop) then initBackdrop()
if not isValid(m.itemIcon) then initItemIcon()
if not isValid(m.playedIndicator) then initPlayedIndicator()
m.itemPoster.width = itemData.imageWidth
m.itemText.maxWidth = itemData.imageWidth
@ -83,11 +93,14 @@ sub itemContentChanged()
if LCase(itemData.type) = "series"
if isValid(userSettings)
if not userSettings["ui.tvshows.disableUnwatchedEpisodeCount"]
unwatchedEpisodeCountSetting = userSettings["ui.tvshows.disableUnwatchedEpisodeCount"]
if isValid(unwatchedEpisodeCountSetting) and not unwatchedEpisodeCountSetting
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
if itemData.json.UserData.UnplayedItemCount > 0
if isValid(m.unplayedCount) then m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
if isValid(m.unplayedEpisodeCount)
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
end if
end if
end if
end if

View File

@ -28,7 +28,9 @@ sub itemContentChanged()
item.selectedVideoStreamId = itemData.MediaSources[0].id
end if
if isValid(itemData.indexNumber)
if isValid(itemData.parentIndexNumber) and itemData.parentIndexNumber = 0
indexNumber = `${tr("Special")} - `
else if isValid(itemData.indexNumber)
indexNumber = `${itemData.indexNumber}. `
if isValid(itemData.indexNumberEnd)
indexNumber = `${itemData.indexNumber}-${itemData.indexNumberEnd}. `

View File

@ -181,9 +181,12 @@ end function
sub onShuffleEpisodeDataLoaded()
m.getShuffleEpisodesTask.unobserveField("data")
queueManager = m.global.queueManager
queueManager.callFunc("set", m.getShuffleEpisodesTask.data.items)
queueManager.callFunc("playQueue")
if isValid(m.getShuffleEpisodesTask.data)
queueManager = m.global.queueManager
queueManager.callFunc("set", m.getShuffleEpisodesTask.data.items)
queueManager.callFunc("playQueue")
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean

View File

@ -1,5 +1,6 @@
import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/session.bs"
import "pkg:/source/roku_modules/log/LogMixin.brs"
sub init()
@ -105,6 +106,7 @@ sub handleItemSkipAction(action as string)
' If there is something next in the queue, play it
if queueManager.callFunc("getPosition") < queueManager.callFunc("getCount") - 1
m.top.control = "stop"
session.video.Delete()
m.global.sceneManager.callFunc("clearPreviousScene")
queueManager.callFunc("moveForward")
queueManager.callFunc("playQueue")
@ -119,6 +121,7 @@ sub handleItemSkipAction(action as string)
' If there is something previous in the queue, play it
if queueManager.callFunc("getPosition") > 0
m.top.control = "stop"
session.video.Delete()
m.global.sceneManager.callFunc("clearPreviousScene")
queueManager.callFunc("moveBack")
queueManager.callFunc("playQueue")
@ -617,19 +620,12 @@ sub onState(msg)
' Pass video state into OSD
m.osd.playbackState = m.top.state
' When buffering, start timer to monitor buffering process
if m.top.state = "buffering"
' start buffer timer
' When buffering, start timer to monitor buffering process
if isValid(m.bufferCheckTimer)
m.bufferCheckTimer.control = "start"
m.bufferCheckTimer.ObserveField("fire", "bufferCheck")
end if
' update server if needed
if not m.playReported
m.playReported = true
ReportPlayback("start")
end if
else if m.top.state = "error"
m.log.error(m.top.errorCode, m.top.errorMsg, m.top.errorStr, m.top.errorCode)
@ -640,10 +636,10 @@ sub onState(msg)
else
' If an error was encountered, Display dialog
showPlaybackErrorDialog(tr("Error During Playback"))
session.video.Delete()
end if
' Stop playback and exit player
m.top.control = "stop"
else if m.top.state = "playing"
' Check if next episode is available
@ -669,9 +665,11 @@ sub onState(msg)
m.playbackTimer.control = "stop"
ReportPlayback("stop")
m.playReported = false
session.video.Delete()
else if m.top.state = "finished"
m.playbackTimer.control = "stop"
ReportPlayback("finished")
session.video.Delete()
else
m.log.warning("Unhandled state", m.top.state, m.playReported, m.playFinished)
end if
@ -730,6 +728,7 @@ sub bufferCheck(msg)
' Stop playback and exit player
m.top.control = "stop"
session.video.Delete()
end if
end if
@ -799,6 +798,7 @@ 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.control = "stop"
m.top.state = "finished"
session.video.Delete()
hideNextEpisodeButton()
return true
else
@ -873,6 +873,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
if key = "back"
m.top.control = "stop"
session.video.Delete()
end if
return false

View File

@ -6,7 +6,7 @@ import "pkg:/source/utils/config.bs"
sub init()
m.itemPoster = m.top.findNode("itemPoster")
m.posterText = m.top.findNode("posterText")
m.title = m.top.findNode("title")
initTitle()
m.posterText.font.size = 30
m.title.font.size = 25
m.backdrop = m.top.findNode("backdrop")
@ -25,6 +25,10 @@ sub init()
end if
end sub
sub initTitle()
m.title = m.top.findNode("title")
end sub
sub itemContentChanged()
m.backdrop.blendColor = "#101010"
@ -56,6 +60,8 @@ sub itemContentChanged()
end sub
sub focusChanged()
if not isValid(m.title) then initTitle()
if m.top.itemHasFocus = true
m.title.repeatCount = -1
else

View File

@ -8,6 +8,7 @@ import "pkg:/source/utils/config.bs"
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/userauth.bs"
import "pkg:/source/utils/deviceCapabilities.bs"
import "pkg:/source/utils/session.bs"
enum SubtitleSelection
notset = -2
@ -73,14 +74,26 @@ end function
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean)
meta = ItemMetaData(video.id)
subtitle_idx = m.top.selectedSubtitleIndex
if not isValid(meta)
video.errorMsg = "Error loading metadata"
video.content = invalid
return
end if
session.video.Update(meta)
if isValid(meta.json.MediaSources[0].RunTimeTicks)
if meta.json.MediaSources[0].RunTimeTicks = 0
video.length = 0
else
video.length = meta.json.MediaSources[0].RunTimeTicks / 10000000
end if
end if
if isValid(meta.json.MediaSources[0]) and isValid(meta.json.MediaSources[0].MediaStreams[0])
video.MaxVideoDecodeResolution = [meta.json.MediaSources[0].MediaStreams[0].Width, meta.json.MediaSources[0].MediaStreams[0].Height]
end if
subtitle_idx = m.top.selectedSubtitleIndex
videotype = LCase(meta.type)
' Check for any Live TV streams or Recordings coming from other places other than the TV Guide
@ -199,12 +212,10 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
}
end if
' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay
fully_external = false
' For h264/hevc video, Roku spec states that it supports specfic encoding levels
' The device can decode content with a Higher Encoding level but may play it back with certain
' artifacts. If the user preference is set, and the only reason the server says we need to
@ -353,11 +364,15 @@ sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
if protocol &lt;> "file"
uri = parseUrl(m.playbackInfo.MediaSources[0].Path)
if isLocalhost(uri[2])
if not isValidAndNotEmpty(uri) then return
if isValid(uri[2]) and isLocalhost(uri[2])
' if the domain of the URI is local to the server,
' create a new URI by appending the received path to the server URL
' later we will substitute the users provided URL for this case
video.content.url = buildURL(uri[4])
if isValid(uri[4])
video.content.url = buildURL(uri[4])
end if
else
fully_external = true
video.content.url = m.playbackInfo.MediaSources[0].Path

View File

@ -20,10 +20,8 @@ sub init()
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
end if
m.gridTitles = m.global.session.user.settings["itemgrid.gridTitles"]
m.posterText.visible = false
m.postTextBackground.visible = false
end sub
sub itemContentChanged()

File diff suppressed because one or more lines are too long

View File

@ -6,9 +6,12 @@ import "pkg:/source/utils/config.bs"
sub setFields()
json = m.top.json
m.top.Type = "Person"
if json = invalid then return
m.top.id = json.id
m.top.favorite = json.UserData.isFavorite
m.top.Type = "Person"
setPoster()
end sub

View File

@ -12,12 +12,12 @@ sub init()
initItemPoster()
m.itemProgress = m.top.findNode("progress")
m.itemProgressBackground = m.top.findNode("progressBackground")
m.itemIcon = m.top.findNode("itemIcon")
initItemIcon()
initItemTextExtra()
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
m.unplayedCount = m.top.findNode("unplayedCount")
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
m.playedIndicator = m.top.findNode("playedIndicator")
initPlayedIndicator()
m.showProgressBarAnimation = m.top.findNode("showProgressBar")
m.showProgressBarField = m.top.findNode("showProgressBarField")
@ -52,6 +52,14 @@ sub initBackdrop()
m.backdrop = m.top.findNode("backdrop")
end sub
sub initItemIcon()
m.itemIcon = m.top.findNode("itemIcon")
end sub
sub initPlayedIndicator()
m.playedIndicator = m.top.findNode("playedIndicator")
end sub
sub itemContentChanged()
if isValid(m.unplayedCount) then m.unplayedCount.visible = false
itemData = m.top.itemContent
@ -65,6 +73,8 @@ sub itemContentChanged()
if not isValid(m.itemText) then initItemText()
if not isValid(m.itemTextExtra) then initItemTextExtra()
if not isValid(m.backdrop) then initBackdrop()
if not isValid(m.itemIcon) then initItemIcon()
if not isValid(m.playedIndicator) then initPlayedIndicator()
m.itemPoster.width = itemData.imageWidth
m.itemText.maxWidth = itemData.imageWidth
@ -85,11 +95,14 @@ sub itemContentChanged()
if LCase(itemData.type) = "series"
if isValid(localGlobal) and isValid(localGlobal.session) and isValid(localGlobal.session.user) and isValid(localGlobal.session.user.settings)
if not localGlobal.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"]
unwatchedEpisodeCountSetting = localGlobal.session.user.settings["ui.tvshows.disableUnwatchedEpisodeCount"]
if isValid(unwatchedEpisodeCountSetting) and not unwatchedEpisodeCountSetting
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
if itemData.json.UserData.UnplayedItemCount > 0
if isValid(m.unplayedCount) then m.unplayedCount.visible = true
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
if isValid(m.unplayedEpisodeCount)
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
end if
end if
end if
end if

View File

@ -29,7 +29,9 @@ sub itemContentChanged()
item.selectedVideoStreamId = itemData.MediaSources[0].id
end if
if isValid(itemData.indexNumber)
if isValid(itemData.parentIndexNumber) and itemData.parentIndexNumber = 0
indexNumber = `${tr("Special")} - `
else if isValid(itemData.indexNumber)
indexNumber = `${itemData.indexNumber}. `
if isValid(itemData.indexNumberEnd)
indexNumber = `${itemData.indexNumber}-${itemData.indexNumberEnd}. `

View File

@ -183,8 +183,10 @@ end function
sub onShuffleEpisodeDataLoaded()
m.getShuffleEpisodesTask.unobserveField("data")
m.global.queueManager.callFunc("set", m.getShuffleEpisodesTask.data.items)
m.global.queueManager.callFunc("playQueue")
if isValid(m.getShuffleEpisodesTask.data)
m.global.queueManager.callFunc("set", m.getShuffleEpisodesTask.data.items)
m.global.queueManager.callFunc("playQueue")
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -226,8 +226,12 @@
' Find the object in the scene's data and update its json data
for i = 0 to currentScene.objects.Items.count() - 1
if LCase(currentScene.objects.Items[i].id) = LCase(currentEpisode.id)
currentScene.objects.Items[i].json = api.users.GetItem(m.global.session.user.id, currentEpisode.id)
m.global.queueManager.callFunc("setTopStartingPoint", currentScene.objects.Items[i].json.UserData.PlaybackPositionTicks)
data = api.users.GetItem(m.global.session.user.id, currentEpisode.id)
if isValid(data)
currentScene.objects.Items[i].json = data
m.global.queueManager.callFunc("setTopStartingPoint", data.UserData.PlaybackPositionTicks)
end if
exit for
end if
end for
@ -246,15 +250,19 @@
currentScene = m.global.sceneManager.callFunc("getActiveScene")
if isValid(currentScene) and isValid(currentScene.itemContent) and isValid(currentScene.itemContent.id)
' Refresh movie detail data
currentScene.itemContent.json = api.users.GetItem(m.global.session.user.id, currentScene.itemContent.id)
movieMetaData = ItemMetaData(currentScene.itemContent.id)
data = api.users.GetItem(m.global.session.user.id, currentScene.itemContent.id)
if isValid(data)
currentScene.itemContent.json = data
' Set updated starting point for the queue item
m.global.queueManager.callFunc("setTopStartingPoint", data.UserData.PlaybackPositionTicks)
' Redraw movie poster
currentScene.newPosterImageURI = movieMetaData.posterURL
' Set updated starting point for the queue item
m.global.queueManager.callFunc("setTopStartingPoint", currentScene.itemContent.json.UserData.PlaybackPositionTicks)
' Refresh movie detail data
movieMetaData = ItemMetaData(currentScene.itemContent.id)
if isValid(movieMetaData)
' Redraw movie poster
currentScene.newPosterImageURI = movieMetaData.posterURL
end if
end if
end if
stopLoadingSpinner()
@ -579,36 +587,41 @@
' If a button is selected, we have some determining to do
btn = getButton(msg)
group = sceneManager.callFunc("getActiveScene")
if isValid(btn) and btn.id = "play-button"
if not isValid(group) then return
' User chose Play button from movie detail view
startLoadingSpinner()
' Check if a specific Audio Stream was selected
audio_stream_idx = 0
if isValid(group) and isValid(group.selectedAudioStreamIndex)
if isValid(group.selectedAudioStreamIndex)
audio_stream_idx = group.selectedAudioStreamIndex
end if
group.itemContent.selectedAudioStreamIndex = audio_stream_idx
group.itemContent.id = group.selectedVideoStreamId
if isValid(group.itemContent)
group.itemContent.selectedAudioStreamIndex = audio_stream_idx
group.itemContent.id = group.selectedVideoStreamId
' Display playback options dialog
if group.itemContent.json.userdata.PlaybackPositionTicks > 0
m.global.queueManager.callFunc("hold", group.itemContent)
playbackOptionDialog(group.itemContent.json.userdata.PlaybackPositionTicks, group.itemContent.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", group.itemContent)
m.global.queueManager.callFunc("playQueue")
' Display playback options dialog
if group.itemContent.json.userdata.PlaybackPositionTicks > 0
m.global.queueManager.callFunc("hold", group.itemContent)
playbackOptionDialog(group.itemContent.json.userdata.PlaybackPositionTicks, group.itemContent.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", group.itemContent)
m.global.queueManager.callFunc("playQueue")
end if
end if
if isValid(group) and isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group"
if isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group"
buttons = group.findNode("buttons")
if isValid(buttons)
group.lastFocus = group.findNode("buttons")
end if
end if
if isValid(group) and isValid(group.lastFocus)
if isValid(group.lastFocus)
group.lastFocus.setFocus(true)
end if

View File

@ -265,16 +265,20 @@ sub AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
if protocol &lt;> "file"
uri = parseUrl(m.playbackInfo.MediaSources[0].Path)
if isLocalhost(uri[2])
if not isValidAndNotEmpty(uri) then return
if isValid(uri[2]) and isLocalhost(uri[2])
' the domain of the URI is local to the server.
' create a new URI by appending the received path to the server URL
' later we will substitute the users provided URL for this case
video.content.url = buildURL(uri[4])
if isValid(uri[4])
video.content.url = buildURL(uri[4])
end if
else
fully_external = true
video.content.url = m.playbackInfo.MediaSources[0].Path
end if
else:
else
params.append({
"Static": "true",
"Container": video.container,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -99,11 +99,8 @@ sub SaveDeviceToGlobal()
print "ERROR parsing deviceInfo.GetVideoMode()"
end if
videoWidth = heightToWidth[videoHeight]
if videoHeight = "2160" and extraData = "b10"
bitDepth = 10
else if videoHeight = "4320"
bitDepth = 12
end if
if extraData &lt;> invalid and extraData = "b10" then bitDepth = 10
if videoHeight = "4320" then bitDepth = 12
m.global.addFields({
device: {

View File

@ -1314,6 +1314,14 @@
<translation>Use Show Image</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>Play Next Episode Automatically</source>
<translation>Play Next Episode Automatically</translation>
</message>
<message>
<source>When finished playing a single episode, play the next one automatically.</source>
<translation>When finished playing a single episode, play the next one automatically.</translation>
</message>
</context>
<context>
<name></name>

View File

@ -1321,5 +1321,10 @@
<translation>Use Show Image</translation>
<extracomment>User Setting - Setting option title</extracomment>
</message>
<message>
<source>Special</source>
<translation>Special</translation>
<extracomment>Special episode of a TV Show</extracomment>
</message>
</context>
</TS>

View File

@ -106,7 +106,7 @@
</message>
<message>
<source>Latest in</source>
<translation type="unfinished">Dernières entrées</translation>
<translation>Dernières entrées</translation>
</message>
<message>
<source>Home</source>
@ -195,7 +195,7 @@
</message>
<message>
<source>Extras</source>
<translation type="unfinished">Bonus</translation>
<translation>Bonus</translation>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
@ -302,7 +302,7 @@
</message>
<message>
<source>CRITIC_RATING</source>
<translation type="unfinished">Note des critiques</translation>
<translation>Note des critiques</translation>
</message>
<message>
<source>DATE_PLAYED</source>
@ -437,7 +437,7 @@
</message>
<message>
<source>View Channel</source>
<translation type="unfinished">Afficher la chaîne</translation>
<translation>Afficher la chaîne</translation>
</message>
<message>
<source>Record</source>
@ -730,7 +730,7 @@
</message>
<message>
<source>Hides tagline text on details pages.</source>
<translation type="unfinished">Masque le texte de l&apos;accroche sur les pages de détails.</translation>
<translation>Masque le texte de l&apos;accroche sur les pages de détails.</translation>
</message>
<message>
<source>Options for TV Shows.</source>
@ -744,7 +744,7 @@
</message>
<message>
<source>Attempt Direct Play for H.264 media with unsupported profile levels before falling back to transcoding if it fails.</source>
<translation type="unfinished">Essayer Lecture directe pour les médias en H.264 avec des profils non supportés avant de se tourner vers la conversion si ça échoue.</translation>
<translation>Essayer Lecture directe pour les médias en H.264 avec des profils non supportés avant de se tourner vers la conversion si ça échoue.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
@ -774,6 +774,302 @@
<translation>Sauter les détails pour les saisons uniques</translation>
<extracomment>Settings Menu - Title for option</extracomment>
</message>
<message>
<source>Show On Hover</source>
<translation>Afficher au survol</translation>
</message>
<message>
<source>Years</source>
<translation>Années</translation>
</message>
<message>
<source>Always Hide</source>
<translation>Toujours masquer</translation>
</message>
<message>
<source>Album Artists (Presentation)</source>
<translation>Artistes de l&apos;album (Diaporama)</translation>
</message>
<message>
<source>Enable Limit</source>
<translation>Activer la limite</translation>
</message>
<message>
<source>Settings that apply when Grid views are enabled.</source>
<translation>Paramètres s&apos;appliquant lorsque les vues en grille sont activées.</translation>
</message>
<message>
<source>Custom Subtitles</source>
<translation>Sous-titres Personnalisés</translation>
</message>
<message>
<source>Show What&apos;s New popup when Jellyfin is updated to a new version.</source>
<translation>Afficher la fenêtre contextuelle des nouveautés lorsque que Jellyfin est mise à jour avec la dernière version.</translation>
</message>
<message>
<source>Select when to show titles.</source>
<translation>Sélectionner quand afficher les titres.</translation>
</message>
<message>
<source>Transcoding Information</source>
<translation>Informations de Transcodage</translation>
</message>
<message>
<source>Audio Channels</source>
<translation>Canaux Audio</translation>
</message>
<message>
<source>Stream Information</source>
<translation>Informations du flux</translation>
</message>
<message>
<source>Codec Tag</source>
<translation>Balise de codec</translation>
</message>
<message>
<source>Songs</source>
<translation>Chansons</translation>
</message>
<message>
<source>View All</source>
<translation type="unfinished">Tout voir</translation>
</message>
<message>
<source>Configure the maximum playback bitrate.</source>
<translation>Configurer le débit binaire maximum de lecture.</translation>
</message>
<message>
<source>Enable or disable the &apos;Maximum Bitrate&apos; setting.</source>
<translation>Activer ou désactiver le paramètre &apos;Débit binaire maximal&apos;.</translation>
</message>
<message>
<source>Set the maximum bitrate in Mbps. Set to 0 to use Roku&apos;s specifications. This setting must be enabled to take effect.</source>
<translation>Définir le débit binaire maximum en Mbps. Régler sur 0 pour utiliser les spécifications de Roku. Ce paramètre doit être activé pour être pris en compte.</translation>
</message>
<message>
<source>Settings relating to the appearance of pages in TV Libraries.</source>
<translation>Paramètres relatifs à l&apos;apparence des pages dans la médiathèque séries TV.</translation>
</message>
<message>
<source>all</source>
<translation>tout</translation>
</message>
<message>
<source>Default view for Movie Libraries.</source>
<translation>Vue par défaut pour les médiathèques de films.</translation>
</message>
<message>
<source>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to transcoding if it fails.</source>
<translation>Essayer la lecture directe des médias HEVC comportant des niveaux de profils incompatibles avant de revenir au transcodage en cas d&apos;échec.</translation>
</message>
<message>
<source>Unable to find any albums or songs belonging to this artist</source>
<translation>Aucun album ou chanson de cet artiste n&apos;a pu être trouvé</translation>
</message>
<message>
<source>Replace Roku&apos;s default subtitle functions with custom functions that support CJK fonts. Fallback fonts must be configured and enabled on the server for CJK rendering to work.</source>
<translation>Remplacez les fonctions de sous-titres par défaut de Roku par des fonctions personnalisées prenant en charge les polices CJK. Les polices de secours doivent être configurées et activées sur le serveur pour que le rendu CJK fonctionne.</translation>
</message>
<message>
<source>Slideshow Off</source>
<translation>Diaporama Désactivé</translation>
</message>
<message>
<source>Parental Ratings</source>
<translation>Classifications parentales</translation>
</message>
<message>
<source>Resumable</source>
<translation>Reprise possible</translation>
</message>
<message>
<source>Movie Library Default View</source>
<translation>Vue par défaut pour la médiathèque de films</translation>
</message>
<message>
<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>Cacher l&apos;étoile et la notation de la communauté pour les épisodes d&apos;une série TV. Cela permet d&apos;éviter les spoilers sur un bon/mauvais épisode à venir.</translation>
</message>
<message>
<source>Maximum Bitrate</source>
<translation>Débit binaire maximum</translation>
</message>
<message>
<source>Bitrate Limit</source>
<translation>Limite de débit binaire</translation>
</message>
<message>
<source>Settings relating to the appearance of the Home screen and the program in general.</source>
<translation>Paramètres relatifs à l&apos;apparence de la page d&apos;accueil et du programme en général.</translation>
</message>
<message>
<source>Artists (Presentation)</source>
<translation>Artistes (diaporama)</translation>
</message>
<message>
<source>Artists (Grid)</source>
<translation>Artistes (grille)</translation>
</message>
<message>
<source>Song</source>
<translation>Chanson</translation>
</message>
<message>
<source>Disable Community Rating for Episodes</source>
<translation>Désactiver la notation de la communauté pour les épisodes</translation>
</message>
<message>
<source>Max Days Next Up</source>
<translation>Maximum de jours dans la section «À suivre»</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>direct</source>
<translation>direct</translation>
</message>
<message>
<source>Codec</source>
<translation>Codec</translation>
</message>
<message>
<source>Level</source>
<translation>Niveau</translation>
</message>
<message>
<source>Size</source>
<translation>Taille</translation>
</message>
<message>
<source>Video range type</source>
<translation>Type de plage vidéo</translation>
</message>
<message>
<source>Text Subtitles Only</source>
<translation>Sous-titres texte uniquement</translation>
</message>
<message>
<source>Aired</source>
<translation>Diffusé le</translation>
</message>
<message>
<source>Slideshow On</source>
<translation>Diaporama activé</translation>
</message>
<message>
<source>MPEG-4 Support</source>
<translation>Support du MPEG-4</translation>
</message>
<message>
<source>Unplayed</source>
<translation>Non lu</translation>
</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éfinit le nombre maximal de jours qu&apos;une série peut rester dans la section &apos;À suivre&apos; si elle n&apos;est plus regardée.</translation>
</message>
<message>
<source>Total Bitrate</source>
<translation>Débit Total</translation>
</message>
<message>
<source>Bit Rate</source>
<translation>Débit binaire</translation>
</message>
<message>
<source>Container</source>
<translation>Conteneur</translation>
</message>
<message>
<source>Pixel format</source>
<translation>Format de pixel</translation>
</message>
<message>
<source>WxH</source>
<translation>L × H</translation>
</message>
<message>
<source>Random Off</source>
<translation>Aléatoire désactivée</translation>
</message>
<message>
<source>Playback Information</source>
<translation>Informations de Lecture</translation>
</message>
<message>
<source>Reason</source>
<translation>Raison</translation>
</message>
<message>
<source>Only display text subtitles to minimize transcoding.</source>
<translation>Affiche seulement des sous-titres texte pour minimiser l&apos;impact du transcodage.</translation>
</message>
<message>
<source>Slideshow Paused</source>
<translation>Diaporama en Pause</translation>
</message>
<message>
<source>Random On</source>
<translation>Aléatoire activée</translation>
</message>
<message>
<source>Slideshow Resumed</source>
<translation>Reprise du diaporama</translation>
</message>
<message>
<source>Show What&apos;s New Popup</source>
<translation>Afficher la fenêtre contextuelle des nouveautés</translation>
</message>
<message>
<source>Played</source>
<translation>Lu</translation>
</message>
<message>
<source>Item Titles</source>
<translation>Titres des éléments</translation>
</message>
<message>
<source>Always Show</source>
<translation>Toujours afficher</translation>
</message>
<message>
<source>Album</source>
<translation type="unfinished">Album</translation>
</message>
<message>
<source>Album Artists (Grid)</source>
<translation>Artistes de l&apos;album (grille)</translation>
</message>
<message>
<source>Albums</source>
<translation>Albums</translation>
</message>
<message>
<source>Biographical information for this person is not currently available.</source>
<translation>Les informations biographiques pour cette personne sont indisponibles pour le moment.</translation>
</message>
<message>
<source>Libraries</source>
<translation>Médiathèques</translation>
</message>
<message>
<source>Settings relating to the appearance of Library pages</source>
<translation>Paramètres relatifs à l&apos;apparence des pages de médiathèque</translation>
</message>
<message>
<source>General</source>
<translation>Général</translation>
</message>
<message>
<source>Grid View Settings</source>
<translation>Paramètres de la vue Grille</translation>
</message>
</context>
<context>
<name></name>

View File

@ -3,7 +3,7 @@
title=Jellyfin
major_version=2
minor_version=1
build_version=5
build_version=8
### Main Menu Icons / Channel Poster Artwork

447
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "jellyfin-roku",
"version": "2.1.5",
"version": "2.1.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jellyfin-roku",
"version": "2.1.5",
"version": "2.1.8",
"hasInstallScript": true,
"license": "GPL-2.0",
"dependencies": {
@ -25,7 +25,7 @@
"rimraf": "6.0.1",
"roku-deploy": "3.12.1",
"roku-log-bsc-plugin": "0.8.1",
"rooibos-roku": "5.13.0",
"rooibos-roku": "5.14.0",
"ropm": "0.10.26",
"spellchecker-cli": "6.2.0",
"undent": "0.1.0"
@ -494,6 +494,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@types/caseless": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@ -556,6 +563,26 @@
"integrity": "sha512-zvSN2Esek1aeLdKDYuntKAYjti9Z2oT4I8bfkLLhIxHlv3dwZ5vvATxOc31820iYm4hQRCwjUgDpwSMFjfTUnw==",
"dev": true
},
"node_modules/@types/request": {
"version": "2.48.12",
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz",
"integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/caseless": "*",
"@types/node": "*",
"@types/tough-cookie": "*",
"form-data": "^2.5.0"
}
},
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/unist": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
@ -746,6 +773,16 @@
"node": ">=0.8"
}
},
"node_modules/async": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash": "^4.17.14"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -1417,6 +1454,13 @@
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz",
"integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw=="
},
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
"dev": true,
"license": "MIT"
},
"node_modules/debounce-promise": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz",
@ -1694,6 +1738,15 @@
"safer-buffer": "^2.1.0"
}
},
"node_modules/emitter-component": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz",
"integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -1721,6 +1774,13 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/eol": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz",
"integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==",
"dev": true,
"license": "MIT"
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -1857,6 +1917,26 @@
"node": ">=8"
}
},
"node_modules/find": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/find/-/find-0.1.7.tgz",
"integrity": "sha512-jPrupTOe/pO//3a9Ty2o4NqQCp0L46UG+swUnfFtdmtQVN8pEltKpAqR7Nuf6vWn0GBXx5w+R1MyZzqwjEIqdA==",
"dev": true,
"dependencies": {
"traverse-chain": "~0.1.0"
}
},
"node_modules/find-in-files": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/find-in-files/-/find-in-files-0.5.0.tgz",
"integrity": "sha512-VraTc6HdtdSHmAp0yJpAy20yPttGKzyBWc7b7FPnnsX9TOgmKx0g9xajizpF/iuu4IvNK4TP0SpyBT9zAlwG+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"find": "^0.1.5",
"q": "^1.0.1"
}
},
"node_modules/find-replace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz",
@ -1905,6 +1985,21 @@
"node": "*"
}
},
"node_modules/form-data": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/format": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
@ -3722,6 +3817,23 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/natural-orderby": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz",
"integrity": "sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/net": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz",
"integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/nlcst-affix-emoticon-modifier": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nlcst-affix-emoticon-modifier/-/nlcst-affix-emoticon-modifier-2.1.1.tgz",
@ -4247,6 +4359,31 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/portfinder": {
"version": "1.0.32",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
"integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"async": "^2.6.4",
"debug": "^3.2.7",
"mkdirp": "^0.5.6"
},
"engines": {
"node": ">= 0.12.0"
}
},
"node_modules/portfinder/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/postman-request": {
"version": "2.88.1-postman.32",
"resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.32.tgz",
@ -4341,6 +4478,18 @@
"node": ">=6"
}
},
"node_modules/q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
"deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6.0",
"teleport": ">=0.2.0"
}
},
"node_modules/qs": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
@ -4538,6 +4687,74 @@
"node": ">= 0.10"
}
},
"node_modules/replace-in-file": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.5.tgz",
"integrity": "sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"glob": "^7.2.0",
"yargs": "^17.2.1"
},
"bin": {
"replace-in-file": "bin/cli.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/replace-in-file/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/replace-in-file/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/replace-in-file/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/replace-last": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/replace-last/-/replace-last-1.2.6.tgz",
"integrity": "sha512-Cj+MK38VtNu1S5J73mEZY3ciQb9dJajNq1Q8inP4dn/MhJMjHwoAF3Z3FjspwAEV9pfABl565MQucmrjOkty4g==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -4952,6 +5169,95 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/roku-debug": {
"version": "0.21.11",
"resolved": "https://registry.npmjs.org/roku-debug/-/roku-debug-0.21.11.tgz",
"integrity": "sha512-OXFKmU91hefyWc5QKYz7bGYjWYntNo8/tOaPwYwDRQnNSYoXYCPViUjQV4VCpOFp7Xj7VJjJIFKwTK/BmvSAQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rokucommunity/logger": "^0.3.9",
"@types/request": "^2.48.8",
"brighterscript": "^0.67.7",
"dateformat": "^4.6.3",
"debounce": "^1.2.1",
"eol": "^0.9.1",
"eventemitter3": "^4.0.7",
"fast-glob": "^3.2.11",
"find-in-files": "^0.5.0",
"fs-extra": "^10.0.0",
"natural-orderby": "^2.0.3",
"portfinder": "^1.0.32",
"postman-request": "^2.88.1-postman.32",
"replace-in-file": "^6.3.2",
"replace-last": "^1.2.6",
"roku-deploy": "^3.12.1",
"semver": "^7.5.4",
"serialize-error": "^8.1.0",
"smart-buffer": "^4.2.0",
"source-map": "^0.7.4",
"telnet-client": "^1.4.9",
"vscode-debugadapter": "^1.49.0",
"vscode-debugprotocol": "^1.49.0",
"vscode-languageserver": "^6.1.1",
"xml2js": "^0.5.0"
},
"bin": {
"roku-debug": "dist/cli.js"
}
},
"node_modules/roku-debug/node_modules/dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/roku-debug/node_modules/serialize-error": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz",
"integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/roku-debug/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/roku-debug/node_modules/vscode-languageserver": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz",
"integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"vscode-languageserver-protocol": "^3.15.3"
},
"bin": {
"installServerIntoExtension": "bin/installServerIntoExtension"
}
},
"node_modules/roku-deploy": {
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.12.1.tgz",
@ -5043,26 +5349,22 @@
"dev": true
},
"node_modules/rooibos-roku": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/rooibos-roku/-/rooibos-roku-5.13.0.tgz",
"integrity": "sha512-tp+hlAtYVnzF3ZMatSmodGl990+pzKg0uwSqM8dyFBNn0Ob5PdUGFRv0oQKg3m7FYCjnglIQxiL97VpiT4rOMg==",
"version": "5.14.0",
"resolved": "https://registry.npmjs.org/rooibos-roku/-/rooibos-roku-5.14.0.tgz",
"integrity": "sha512-eSAqO5J2IKYn08ZSFji/DGlD/7mX8wYD7/E0HgxQrWIFoxggR8e/o+8ljxnp/udcJ5aATbBgUi8xd89asyVmCQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"roku-debug": "^0.21.10",
"roku-deploy": "^3.12.1",
"source-map": "^0.7.3",
"undent": "^0.1.0",
"vscode-languageserver": "~6.1.1",
"vscode-languageserver-protocol": "~3.15.3"
}
},
"node_modules/rooibos-roku/node_modules/vscode-jsonrpc": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.0.0 || >=10.0.0"
"vscode-languageserver-protocol": "~3.17.5",
"yargs": "^16.2.0"
},
"bin": {
"rooibos": "dist/cli.js"
}
},
"node_modules/rooibos-roku/node_modules/vscode-languageserver": {
@ -5078,24 +5380,25 @@
"installServerIntoExtension": "bin/installServerIntoExtension"
}
},
"node_modules/rooibos-roku/node_modules/vscode-languageserver-protocol": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
"node_modules/rooibos-roku/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"license": "MIT",
"dependencies": {
"vscode-jsonrpc": "^5.0.1",
"vscode-languageserver-types": "3.15.1"
"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/rooibos-roku/node_modules/vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/ropm": {
"version": "0.10.26",
"resolved": "https://registry.npmjs.org/ropm/-/ropm-0.10.26.tgz",
@ -5362,6 +5665,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
@ -5471,6 +5785,16 @@
"node": ">=0.10.0"
}
},
"node_modules/stream": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz",
"integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emitter-component": "^1.1.1"
}
},
"node_modules/stream-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz",
@ -5619,6 +5943,29 @@
"integrity": "sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg==",
"dev": true
},
"node_modules/telnet-client": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-1.4.11.tgz",
"integrity": "sha512-m15pRh7F74ZzWmqjUtOg3SYp8iSnH7xY1lD9hWk8icjuLRSItABPk1vRJFwSM7op6TgZuEXOWOSbiK9nknloSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"bluebird": "^3.5.4",
"net": "^1.0.2",
"stream": "^0.0.2"
},
"funding": {
"type": "paypal",
"url": "https://paypal.me/kozjak"
}
},
"node_modules/telnet-client/node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true,
"license": "MIT"
},
"node_modules/temp-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
@ -5668,6 +6015,13 @@
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
"dev": true
},
"node_modules/traverse-chain": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",
"integrity": "sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==",
"dev": true,
"license": "MIT"
},
"node_modules/trough": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz",
@ -6090,6 +6444,39 @@
"integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==",
"dev": true
},
"node_modules/vscode-debugadapter": {
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.51.0.tgz",
"integrity": "sha512-mObaXD5/FH/z6aL2GDuyCLbnwLsYRCAJWgFid01vKW9Y5Si8OvINK+Tn+Yl/lRUbetjNuZW3j1euMEz6z8Yzqg==",
"deprecated": "This package has been renamed to @vscode/debugadapter, please update to the new name",
"dev": true,
"license": "MIT",
"dependencies": {
"mkdirp": "^1.0.4",
"vscode-debugprotocol": "1.51.0"
}
},
"node_modules/vscode-debugadapter/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/vscode-debugprotocol": {
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.51.0.tgz",
"integrity": "sha512-dzKWTMMyebIMPF1VYMuuQj7gGFq7guR8AFya0mKacu+ayptJfaRuM0mdHCqiOth4FnRP8mPhEroFPx6Ift8wHA==",
"deprecated": "This package has been renamed to @vscode/debugprotocol, please update to the new name",
"dev": true,
"license": "MIT"
},
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",

View File

@ -1,7 +1,7 @@
{
"name": "jellyfin-roku",
"type": "module",
"version": "2.1.5",
"version": "2.1.8",
"description": "Roku app for Jellyfin media server",
"dependencies": {
"@rokucommunity/bslib": "0.1.1",
@ -19,7 +19,7 @@
"rimraf": "6.0.1",
"roku-deploy": "3.12.1",
"roku-log-bsc-plugin": "0.8.1",
"rooibos-roku": "5.13.0",
"rooibos-roku": "5.14.0",
"ropm": "0.10.26",
"spellchecker-cli": "6.2.0",
"undent": "0.1.0"

View File

@ -25,7 +25,7 @@
"description": "Enable or disable the 'Maximum Bitrate' setting.",
"settingName": "playback.bitrate.maxlimited",
"type": "bool",
"default": "true"
"default": "false"
},
{
"title": "Maximum Bitrate",

View File

@ -224,8 +224,12 @@ sub Main (args as dynamic) as void
' Find the object in the scene's data and update its json data
for i = 0 to currentScene.objects.Items.count() - 1
if LCase(currentScene.objects.Items[i].id) = LCase(currentEpisode.id)
currentScene.objects.Items[i].json = api.users.GetItem(m.global.session.user.id, currentEpisode.id)
m.global.queueManager.callFunc("setTopStartingPoint", currentScene.objects.Items[i].json.UserData.PlaybackPositionTicks)
data = api.users.GetItem(m.global.session.user.id, currentEpisode.id)
if isValid(data)
currentScene.objects.Items[i].json = data
m.global.queueManager.callFunc("setTopStartingPoint", data.UserData.PlaybackPositionTicks)
end if
exit for
end if
end for
@ -244,15 +248,19 @@ sub Main (args as dynamic) as void
currentScene = m.global.sceneManager.callFunc("getActiveScene")
if isValid(currentScene) and isValid(currentScene.itemContent) and isValid(currentScene.itemContent.id)
' Refresh movie detail data
currentScene.itemContent.json = api.users.GetItem(m.global.session.user.id, currentScene.itemContent.id)
movieMetaData = ItemMetaData(currentScene.itemContent.id)
data = api.users.GetItem(m.global.session.user.id, currentScene.itemContent.id)
if isValid(data)
currentScene.itemContent.json = data
' Set updated starting point for the queue item
m.global.queueManager.callFunc("setTopStartingPoint", data.UserData.PlaybackPositionTicks)
' Redraw movie poster
currentScene.newPosterImageURI = movieMetaData.posterURL
' Set updated starting point for the queue item
m.global.queueManager.callFunc("setTopStartingPoint", currentScene.itemContent.json.UserData.PlaybackPositionTicks)
' Refresh movie detail data
movieMetaData = ItemMetaData(currentScene.itemContent.id)
if isValid(movieMetaData)
' Redraw movie poster
currentScene.newPosterImageURI = movieMetaData.posterURL
end if
end if
end if
stopLoadingSpinner()
@ -577,36 +585,41 @@ sub Main (args as dynamic) as void
' If a button is selected, we have some determining to do
btn = getButton(msg)
group = sceneManager.callFunc("getActiveScene")
if isValid(btn) and btn.id = "play-button"
if not isValid(group) then return
' User chose Play button from movie detail view
startLoadingSpinner()
' Check if a specific Audio Stream was selected
audio_stream_idx = 0
if isValid(group) and isValid(group.selectedAudioStreamIndex)
if isValid(group.selectedAudioStreamIndex)
audio_stream_idx = group.selectedAudioStreamIndex
end if
group.itemContent.selectedAudioStreamIndex = audio_stream_idx
group.itemContent.id = group.selectedVideoStreamId
if isValid(group.itemContent)
group.itemContent.selectedAudioStreamIndex = audio_stream_idx
group.itemContent.id = group.selectedVideoStreamId
' Display playback options dialog
if group.itemContent.json.userdata.PlaybackPositionTicks > 0
m.global.queueManager.callFunc("hold", group.itemContent)
playbackOptionDialog(group.itemContent.json.userdata.PlaybackPositionTicks, group.itemContent.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", group.itemContent)
m.global.queueManager.callFunc("playQueue")
' Display playback options dialog
if group.itemContent.json.userdata.PlaybackPositionTicks > 0
m.global.queueManager.callFunc("hold", group.itemContent)
playbackOptionDialog(group.itemContent.json.userdata.PlaybackPositionTicks, group.itemContent.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", group.itemContent)
m.global.queueManager.callFunc("playQueue")
end if
end if
if isValid(group) and isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group"
if isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group"
buttons = group.findNode("buttons")
if isValid(buttons)
group.lastFocus = group.findNode("buttons")
end if
end if
if isValid(group) and isValid(group.lastFocus)
if isValid(group.lastFocus)
group.lastFocus.setFocus(true)
end if

View File

@ -263,16 +263,20 @@ sub AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
if protocol <> "file"
uri = parseUrl(m.playbackInfo.MediaSources[0].Path)
if isLocalhost(uri[2])
if not isValidAndNotEmpty(uri) then return
if isValid(uri[2]) and isLocalhost(uri[2])
' the domain of the URI is local to the server.
' create a new URI by appending the received path to the server URL
' later we will substitute the users provided URL for this case
video.content.url = buildURL(uri[4])
if isValid(uri[4])
video.content.url = buildURL(uri[4])
end if
else
fully_external = true
video.content.url = m.playbackInfo.MediaSources[0].Path
end if
else:
else
params.append({
"Static": "true",
"Container": video.container,

View File

@ -1,4 +1,5 @@
import "pkg:/source/api/sdk.bs"
import "pkg:/source/utils/misc.bs"
function ItemGetPlaybackInfo(id as string, startTimeTicks = 0 as longinteger)
params = {
@ -13,9 +14,6 @@ function ItemGetPlaybackInfo(id as string, startTimeTicks = 0 as longinteger)
end function
function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioTrackIndex = -1 as integer, subtitleTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger)
body = {
"DeviceProfile": getDeviceProfile()
}
params = {
"UserId": m.global.session.user.id,
"StartTimeTicks": startTimeTicks,
@ -25,6 +23,7 @@ function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioT
"MaxStaticBitrate": "140000000",
"SubtitleStreamIndex": subtitleTrackIndex
}
deviceProfile = getDeviceProfile()
' Note: Jellyfin v10.9+ now remuxs LiveTV and does not allow DirectPlay anymore.
' Because of this, we need to tell the server "EnableDirectPlay = false" so that we receive the
@ -38,11 +37,39 @@ function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioT
params.EnableDirectPlay = false
end if
if audioTrackIndex > -1 then params.AudioStreamIndex = audioTrackIndex
myGLobal = m.global
if audioTrackIndex > -1 and myGLobal.session.video.json.MediaStreams <> invalid
selectedAudioStream = myGLobal.session.video.json.MediaStreams[audioTrackIndex]
if selectedAudioStream <> invalid
params.AudioStreamIndex = audioTrackIndex
' force the server to transcode AAC profiles we don't support to MP3 instead of the usual AAC
' TODO: Remove this after server adds support for transcoding AAC from one profile to another
if LCase(selectedAudioStream.Codec) = "aac"
if LCase(selectedAudioStream.Profile) = "main" or LCase(selectedAudioStream.Profile) = "he-aac"
for each rule in deviceProfile.TranscodingProfiles
if rule.Container = "ts" or rule.Container = "mp4"
if rule.AudioCodec = "aac"
rule.AudioCodec = "mp3"
else if rule.AudioCodec.Left(4) = "aac,"
rule.AudioCodec = mid(rule.AudioCodec, 5)
if rule.AudioCodec.Left(3) <> "mp3"
rule.AudioCodec = "mp3," + rule.AudioCodec
end if
end if
end if
end for
end if
end if
end if
end if
req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
req.SetRequest("POST")
return postJson(req, FormatJson(body))
return postJson(req, FormatJson({ "DeviceProfile": deviceProfile }))
end function
' Search across all libraries

View File

@ -1207,7 +1207,7 @@ namespace api
' Moves a playlist item.
function Move(playlistid as string, itemid as string, newindex as integer)
req = APIRequest(Substitute("/playlists/{0}/items/{1}/move/{2}", playlistid, itemid, newindex))
req = APIRequest(Substitute("/playlists/{0}/items/{1}/move/{2}", playlistid, itemid, newindex.ToStr()))
return postVoid(req)
end function
end namespace
@ -2099,7 +2099,7 @@ namespace api
' Gets an HLS subtitle playlist.
function GetHLSSubtitlePlaylistURL(id as string, streamindex as integer, mediasourceid as string, params = {} as object)
return buildURL(Substitute("/videos/{0}/{1}/subtitles/{2}/subtitles.m3u8", id, streamindex, mediasourceid), params)
return buildURL(Substitute("/videos/{0}/{1}/subtitles/{2}/subtitles.m3u8", id, streamindex.ToStr(), mediasourceid), params)
end function
' Upload an external subtitle file.
@ -2115,13 +2115,13 @@ namespace api
' Gets subtitles in a specified format.
function GetSubtitlesWithStartPosition(routeitemid as string, routemediasourceid as string, routeindex as integer, routestartpositionticks as integer, routeformat as string, params = {} as object)
' We maxed out params for substitute() so we must manually add the routeformat value
return buildURL(Substitute("/videos/{0}/{1}/subtitles/{2}/{3}/stream." + routeformat, routeitemid, routemediasourceid, routeindex, routestartpositionticks), params)
return buildURL(Substitute("/videos/{0}/{1}/subtitles/{2}/{3}/stream." + routeformat, routeitemid, routemediasourceid, routeindex.ToStr(), routestartpositionticks.ToStr()), params)
end function
' Gets subtitles in a specified format.
function GetSubtitles(routeitemid as string, routemediasourceid as string, routeindex as integer, routestartpositionticks as integer, routeformat as string, params = {} as object)
' We maxed out params for substitute() so we must manually add the routeformat value
return buildURL(Substitute("/videos/{0}/{1}/subtitles/{2}/{3}/stream." + routeformat, routeitemid, routemediasourceid, routeindex, routestartpositionticks), params)
return buildURL(Substitute("/videos/{0}/{1}/subtitles/{2}/{3}/stream." + routeformat, routeitemid, routemediasourceid, routeindex.ToStr(), routestartpositionticks.ToStr()), params)
end function
end namespace

View File

@ -414,7 +414,9 @@ function getContainerProfiles() as object
end function
function getCodecProfiles() as object
globalUserSettings = m.global.session.user.settings
myGlobal = m.global
globalUserSettings = myGlobal.session.user.settings
codecProfiles = []
profileSupport = {
"h264": {},
@ -451,6 +453,12 @@ function getCodecProfiles() as object
"Value": "Main",
"IsRequired": true
},
{
"Condition": "NotEquals",
"Property": "AudioProfile",
"Value": "HE-AAC",
"IsRequired": true
},
{
"Condition": "LessThanEqual",
"Property": "AudioChannels",
@ -549,26 +557,45 @@ function getCodecProfiles() as object
hevcVideoRangeTypes = "SDR"
vp9VideoRangeTypes = "SDR"
av1VideoRangeTypes = "SDR"
canPlayDovi = false
dp = di.GetDisplayProperties()
if dp.Hdr10
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HDR10"
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HDR10"
av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10"
end if
if dp.Hdr10Plus
av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10+"
end if
if dp.HLG
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HLG"
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HLG"
av1VideoRangeTypes = av1VideoRangeTypes + "|HLG"
end if
if dp.DolbyVision
h264VideoRangeTypes = h264VideoRangeTypes + "|DOVI"
hevcVideoRangeTypes = hevcVideoRangeTypes + "|DOVI"
'vp9VideoRangeTypes = vp9VideoRangeTypes + ",DOVI" no evidence that vp9 can hold DOVI
av1VideoRangeTypes = av1VideoRangeTypes + "|DOVI"
if canPlay4k()
dp = di.GetDisplayProperties()
if dp.DolbyVision
canPlayDovi = true
h264VideoRangeTypes = h264VideoRangeTypes + "|DOVI|DOVIWithSDR"
hevcVideoRangeTypes = hevcVideoRangeTypes + "|DOVI|DOVIWithSDR"
av1VideoRangeTypes = av1VideoRangeTypes + "|DOVI|DOVIWithSDR"
end if
if dp.Hdr10
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HDR10"
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HDR10"
av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10"
if canPlayDovi
hevcVideoRangeTypes = hevcVideoRangeTypes + "|DOVIWithHDR10"
av1VideoRangeTypes = av1VideoRangeTypes + "|DOVIWithHDR10"
end if
end if
if dp.Hdr10Plus
av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10+"
end if
if dp.HLG
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HLG"
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HLG"
av1VideoRangeTypes = av1VideoRangeTypes + "|HLG"
if canPlayDovi
hevcVideoRangeTypes = hevcVideoRangeTypes + "|DOVIWithHLG"
vp9VideoRangeTypes = vp9VideoRangeTypes + "|DOVIWithHLG"
av1VideoRangeTypes = av1VideoRangeTypes + "|DOVIWithHLG"
end if
end if
end if
' H264
@ -599,12 +626,6 @@ function getCodecProfiles() as object
"Value": "true",
"IsRequired": false
},
{
"Condition": "LessThanEqual",
"Property": "VideoBitDepth",
"Value": "8",
"IsRequired": false
},
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
@ -1088,3 +1109,28 @@ function setPreferredCodec(codecString as string, preferredCodec as string) as s
return newCodecString
end if
end function
' does the connected display support playing 4k video?
function canPlay4k() as boolean
deviceInfo = CreateObject("roDeviceInfo")
hdmiStatus = CreateObject("roHdmiStatus")
' Check if the output mode is 2160p or higher
maxVideoHeight = m.global.device.videoHeight
if maxVideoHeight = invalid then return false
if maxVideoHeight.ToInt() < 2160
return false
end if
' Check if HDCP 2.2 is enabled, skip check for TVs
if deviceInfo.GetModelType() = "STB" and hdmiStatus.IsHdcpActive("2.2") <> true
return false
end if
' Check if the Roku player can decode 4K 60fps HEVC streams
if deviceInfo.CanDecodeVideo({ Codec: "hevc", Profile: "main", Level: "5.1" }).result <> true
return false
end if
return true
end function

View File

@ -97,11 +97,8 @@ sub SaveDeviceToGlobal()
print "ERROR parsing deviceInfo.GetVideoMode()"
end if
videoWidth = heightToWidth[videoHeight]
if videoHeight = "2160" and extraData = "b10"
bitDepth = 10
else if videoHeight = "4320"
bitDepth = 12
end if
if extraData <> invalid and extraData = "b10" then bitDepth = 10
if videoHeight = "4320" then bitDepth = 12
m.global.addFields({
device: {

View File

@ -16,6 +16,9 @@ namespace session
Policy: {},
settings: {},
lastRunVersion: invalid
},
video: {
json: {}
}
}
})
@ -31,7 +34,7 @@ namespace session
' Update one value from the global session array (m.global.session)
sub Update(key as string, value = {} as object)
' validate parameters
if key = "" or (key <> "user" and key <> "server") or value = invalid
if key = "" or (key <> "user" and key <> "server" and key <> "video") or value = invalid
print "Error in session.Update(): Invalid parameters provided"
return
end if
@ -430,4 +433,19 @@ namespace session
end sub
end namespace
end namespace
namespace video
' Return the global video session array to it's default state
sub Delete()
session.Update("video", { json: {} })
end sub
' Update the global video session array (m.global.session.video)
sub Update(videoMetaData as object)
if videoMetaData = invalid then return
session.video.Delete()
session.Update("video", videoMetaData)
end sub
end namespace
end namespace