Floatplane channel plugin for Jellyfin 10.10.x: browse, HLS proxy, cookie session, CI release
Some checks failed
release / build-release (push) Has been cancelled
Some checks failed
release / build-release (push) Has been cancelled
- 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>
This commit is contained in:
393
doc/research/reference-sources/hydra_login_screen.brs
Normal file
393
doc/research/reference-sources/hydra_login_screen.brs
Normal file
@@ -0,0 +1,393 @@
|
||||
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
|
||||
Reference in New Issue
Block a user