Files
jellyfin-plugin-floatplane/doc/research/reference-sources/hydra_login_screen.brs
claude 9e33a343ab
Some checks failed
release / build-release (push) Has been cancelled
Floatplane channel plugin for Jellyfin 10.10.x: browse, HLS proxy, cookie session, CI release
- IChannel: creators -> sub-channels -> video posts (public listing API, search mapped)
- IRequiresMediaInfoCallback -> signed proxy URLs; stream controller rewrites HLS so the
  AES-128 key is fetched server-side with the session cookie, segments stay on the CDN
- delivery/info cached per token lifetime and throttled (~2/min)
- config page: paste sails.sid + validate, extra creators, quality, search test
- scripts/package.sh -> plugin folder + zip + md5 + manifest.json; Gitea release workflow
- research docs, HAR analysis (HAR itself gitignored), install-pi5.sh

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 09:42:27 +01:00

394 lines
13 KiB
Plaintext

function init()
m.login_text = m.top.findNode("login_text")
if m.login_text <> invalid
m.login_text.font.size = 50
end if
m.qrCode = m.top.findNode("qrCode")
m.expirationTimerLabel = m.top.findNode("expirationTimerLabel")
m.expirationTimer = m.top.findNode("expirationTimer")
m.orLabel = m.top.findNode("orLabel")
m.orLine = m.top.findNode("orLine")
m.orLine2 = m.top.findNode("orLine2")
m.manualEntryLabel = m.top.findNode("manualEntryLabel")
m.verificationUriBox = m.top.findNode("verificationUriBox")
m.verificationUriLabel = m.top.findNode("verificationUriLabel")
' Set smaller font for URL to prevent truncation
if m.verificationUriLabel <> invalid
m.verificationUriLabel.font.size = 20
end if
m.enterCodeLabel = m.top.findNode("enterCodeLabel")
m.userCodeBox = m.top.findNode("userCodeBox")
m.userCodeLabel = m.top.findNode("userCodeLabel")
' Set horizAlignment programmatically to ensure it's applied
m.contentSectionWrapper = m.top.findNode("contentSectionWrapper")
if m.contentSectionWrapper <> invalid
m.contentSectionWrapper.horizAlignment = "center"
end if
m.contentSection = m.top.findNode("contentSection")
if m.contentSection <> invalid
m.contentSection.horizAlignment = "center"
m.contentSection.vertAlignment = "center"
end if
' Set vertAlignment and horizAlignment programmatically for dividerSection and manualEntrySection
m.dividerSection = m.top.findNode("dividerSection")
if m.dividerSection <> invalid
m.dividerSection.horizAlignment = "center"
m.dividerSection.vertAlignment = "center"
end if
m.manualEntrySection = m.top.findNode("manualEntrySection")
if m.manualEntrySection <> invalid
m.manualEntrySection.horizAlignment = "center"
m.manualEntrySection.vertAlignment = "center"
end if
' Center the labels inside manualEntrySection
if m.manualEntryLabel <> invalid
m.manualEntryLabel.horizAlign = "center"
end if
if m.enterCodeLabel <> invalid
m.enterCodeLabel.horizAlign = "center"
end if
m.showingManualEntry = false
' Timer to trigger next field change asynchronously
m.nextTimer = createObject("roSGNode", "Timer")
m.nextTimer.repeat = false
m.nextTimer.duration = 0.1
m.nextTimer.observeField("fire", "onNextTimerFire")
' Timer for countdown display
m.countdownTimer = createObject("roSGNode", "Timer")
m.countdownTimer.repeat = true
m.countdownTimer.duration = 1.0
m.countdownTimer.observeField("fire", "onCountdownTimerFire")
' Observe reset field to restart OAuth flow
m.top.observeField("reset", "onReset")
' Observe visible field to start OAuth flow when screen becomes visible
m.top.observeField("visible", "onVisibleChanged")
' Track if OAuth flow has been started to avoid starting it multiple times
m.oauthFlowStarted = false
' Rate limiting for OAuth retries
m.oauthRetryCount = 0
m.lastOAuthErrorTime = invalid
m.oauthRetryTimer = createObject("roSGNode", "Timer")
m.oauthRetryTimer.repeat = false
m.oauthRetryTimer.duration = 5.0 ' Wait 5 seconds before retrying
m.oauthRetryTimer.observeField("fire", "onOAuthRetryTimerFire")
' Start OAuth flow if screen is already visible
if m.top.visible = true
startOAuthFlow()
m.oauthFlowStarted = true
end if
end function
sub onVisibleChanged(obj)
if m.top.visible = true and not m.oauthFlowStarted
' Screen became visible and OAuth flow hasn't started yet
' Reset retry counter when screen becomes visible
m.oauthRetryCount = 0
m.lastOAuthErrorTime = invalid
m.oauthRetryTimer.control = "stop"
startOAuthFlow()
m.oauthFlowStarted = true
else if m.top.visible = false
' Screen hidden - stop OAuth flow and reset flag
if m.oauthTask <> invalid
m.oauthTask.control = "STOP"
end if
m.countdownTimer.control = "stop"
m.oauthRetryTimer.control = "stop"
m.oauthFlowStarted = false
end if
end sub
sub onNextTimerFire()
' Set next field asynchronously to ensure observer fires
time = CreateObject("roDateTime")
m.top.next = "beep" + time.AsSeconds().ToStr()
print "[PROGRESS] Set next field via timer to: " + m.top.next
m.nextTimer.control = "stop"
end sub
sub onReset()
if m.top.reset = true
' Reset state
m.showingManualEntry = false
m.field = ""
' Clear the next field so it can trigger again
m.top.next = ""
' Stop existing OAuth task if running
if m.oauthTask <> invalid
m.oauthTask.control = "STOP"
end if
m.countdownTimer.control = "stop"
m.oauthRetryTimer.control = "stop"
' Reset retry counter and error tracking
m.oauthRetryCount = 0
m.lastOAuthErrorTime = invalid
' Reset OAuth flow flag so it can start again
m.oauthFlowStarted = false
' Restart OAuth flow
startOAuthFlow()
m.oauthFlowStarted = true
' Reset the field
m.top.reset = false
end if
end sub
sub refreshQRCode()
' Stop existing OAuth task if running
if m.oauthTask <> invalid
m.oauthTask.control = "STOP"
end if
m.countdownTimer.control = "stop"
m.oauthRetryTimer.control = "stop"
' Switch back to timer view
m.expirationTimerLabel.text = "Time Remaining:"
m.expirationTimer.text = "--:--"
m.expirationTimerLabel.visible = true
m.expirationTimer.visible = true
' Clear manual entry fields temporarily
m.verificationUriLabel.text = ""
m.userCodeLabel.text = ""
m.orLabel.visible = false
m.orLine.visible = false
m.orLine2.visible = false
m.manualEntryLabel.visible = false
m.verificationUriBox.visible = false
m.verificationUriLabel.visible = false
m.enterCodeLabel.visible = false
m.userCodeBox.visible = false
m.userCodeLabel.visible = false
' Restart OAuth flow
m.oauthFlowStarted = false
startOAuthFlow()
m.oauthFlowStarted = true
end sub
sub startOAuthFlow()
' Show QR code by default
m.showingManualEntry = false
m.qrCode.visible = true
' Show timer
m.expirationTimerLabel.text = "Time Remaining:"
m.expirationTimerLabel.visible = true
m.expirationTimer.visible = true
m.expirationTimer.text = "--:--"
m.orLabel.visible = false
m.orLine.visible = false
m.orLine2.visible = false
m.manualEntryLabel.visible = false
m.verificationUriBox.visible = false
m.verificationUriLabel.visible = false
m.enterCodeLabel.visible = false
m.userCodeBox.visible = false
m.userCodeLabel.visible = false
' Start OAuth device flow task
m.oauthTask = createObject("roSGNode", "oauthDeviceTask")
print "[PROGRESS] Setting up OAuth task observers..."
m.oauthTask.observeField("userCode", "onUserCodeReady")
m.oauthTask.observeField("verificationUri", "onVerificationUriReady")
m.oauthTask.observeField("status", "onOAuthStatusChanged")
m.oauthTask.observeField("error", "onOAuthError")
m.oauthTask.observeField("expiresIn", "onExpiresInChanged")
m.oauthTask.control = "RUN"
print "[PROGRESS] OAuth task started"
end sub
sub onUserCodeReady(obj)
userCode = obj.getData()
if userCode <> invalid and userCode <> ""
m.userCodeLabel.text = userCode
print "[PROGRESS] User code: " + userCode
end if
end sub
sub onVerificationUriReady(obj)
verificationUri = obj.getData()
if verificationUri <> invalid and verificationUri <> ""
m.verificationUriLabel.text = verificationUri
m.orLabel.visible = true
m.orLine.visible = true
m.orLine2.visible = true
m.manualEntryLabel.visible = true
m.verificationUriBox.visible = true
m.verificationUriLabel.visible = true
m.enterCodeLabel.visible = true
m.userCodeBox.visible = true
m.userCodeLabel.visible = true
print "[PROGRESS] Verification URI: " + verificationUri
end if
end sub
sub onOAuthStatusChanged(obj)
status = obj.getData()
print "[PROGRESS] OAuth status changed: " + status
' If status is QR_CODE_READY, set the QR code text on the component
if status = "QR_CODE_READY" and m.oauthTask <> invalid
verificationUriComplete = m.oauthTask.verificationUriComplete
if verificationUriComplete <> invalid and verificationUriComplete <> ""
' The qrCode node is the library's QRCode component
' Just set the text field - the component will automatically generate the QR code
m.qrCode.text = verificationUriComplete
end if
end if
status = obj.getData()
if status = "QR_CODE_READY"
m.countdownTimer.control = "start"
' Show timer
m.expirationTimerLabel.visible = true
m.expirationTimer.visible = true
else if status = "AUTHENTICATED"
m.countdownTimer.control = "stop"
m.expirationTimer.text = ""
m.expirationTimerLabel.visible = false
m.expirationTimer.visible = false
' Proceed to main screen
time = CreateObject("roDateTime")
nextValue = "beep" + time.AsSeconds().ToStr()
m.top.next = nextValue
print "[PROGRESS] OAuth authentication complete, proceeding to main screen"
end if
end sub
sub onOAuthError(obj)
error = obj.getData()
' Stop countdown timer
m.countdownTimer.control = "stop"
' Check if this is a client credential error (401 - invalid_client)
' These shouldn't be auto-retried as they indicate a configuration issue
if error <> invalid and error.InStr("invalid_client") >= 0
print "[ERROR] OAuth client credential error - not retrying automatically"
' Show error to user instead of auto-retrying
showErrorDialog("Authentication error: Invalid client credentials. Please check configuration.")
return
end if
' For timeout/expired errors, retry with rate limiting
if error = "TIMEOUT" or error = "EXPIRED"
scheduleOAuthRetry()
else if error <> invalid and error <> ""
' For other errors, check rate limit before retrying
scheduleOAuthRetry()
end if
end sub
sub scheduleOAuthRetry()
' Rate limiting: prevent rapid retries
currentTime = CreateObject("roDateTime")
currentTimeSeconds = currentTime.AsSeconds()
' If we retried recently (within last 5 seconds), wait before retrying again
if m.lastOAuthErrorTime <> invalid
timeSinceLastError = currentTimeSeconds - m.lastOAuthErrorTime
if timeSinceLastError < 5
print "[OAUTH] Rate limiting: waiting before retry (last error was " + timeSinceLastError.ToStr() + " seconds ago)"
' Start retry timer if not already running
if not m.oauthRetryTimer.control = "start"
m.oauthRetryTimer.control = "start"
end if
return
end if
end if
' Increment retry count
m.oauthRetryCount = m.oauthRetryCount + 1
m.lastOAuthErrorTime = currentTimeSeconds
' Limit maximum retries to prevent infinite loops
if m.oauthRetryCount > 10
print "[ERROR] OAuth retry limit reached (" + m.oauthRetryCount.ToStr() + " attempts)"
showErrorDialog("Authentication failed after multiple attempts. Please try again later.")
m.oauthRetryCount = 0 ' Reset counter
return
end if
print "[OAUTH] Scheduling retry #" + m.oauthRetryCount.ToStr()
' Wait a bit before retrying (exponential backoff: 2^retryCount seconds, max 30 seconds)
waitSeconds = 2 ^ m.oauthRetryCount
if waitSeconds > 30
waitSeconds = 30
end if
m.oauthRetryTimer.duration = waitSeconds
m.oauthRetryTimer.control = "start"
end sub
sub onOAuthRetryTimerFire()
m.oauthRetryTimer.control = "stop"
print "[OAUTH] Retrying OAuth flow after delay"
refreshQRCode()
end sub
sub onExpiresInChanged(obj)
expiresIn = obj.getData()
if expiresIn <> invalid and expiresIn <> ""
m.expiresInSeconds = Val(expiresIn)
end if
end sub
sub onCountdownTimerFire()
if m.expiresInSeconds <> invalid
if m.expiresInSeconds > 0
minutes = m.expiresInSeconds \ 60
seconds = m.expiresInSeconds mod 60
timeText = minutes.ToStr() + ":" + Right("0" + seconds.ToStr(), 2)
m.expirationTimer.text = timeText
m.expiresInSeconds = m.expiresInSeconds - 1
else
m.countdownTimer.control = "stop"
' Automatically refresh QR code when timer expires
refreshQRCode()
end if
end if
end sub
' Manual entry removed - OAuth device flow only
sub showErrorDialog(msg)
m.top.getScene().dialog = createObject("roSGNode", "SimpleDialog")
m.top.getScene().dialog.title = "Login Error"
m.top.getScene().dialog.showCancel = false
m.top.getScene().dialog.text = msg
setupDialogPalette()
m.top.getScene().dialog.observeField("buttonSelected","closeDialog")
end sub
sub closeDialog()
m.top.getScene().dialog.close = true
end sub
sub setupDialogPalette()
palette = createObject("roSGNode", "RSGPalette")
palette.colors = { DialogBackgroundColor: "0x152130FF"}
m.top.getScene().dialog.palette = palette
end sub
function onKeyEvent(key as String, press as Boolean) as Boolean
' No key handling needed - auto-refresh handles everything
return false
end function
' Old showInstructions function removed - no longer needed