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>
221 lines
7.8 KiB
Plaintext
221 lines
7.8 KiB
Plaintext
sub init()
|
|
m.top.functionName = "request"
|
|
end sub
|
|
|
|
sub request()
|
|
' Request device authorization
|
|
deviceAuthResponse = requestDeviceAuthorization()
|
|
if deviceAuthResponse = invalid
|
|
' Error details are already logged in requestDeviceAuthorization
|
|
' Set a generic error - the actual error message is in the logs
|
|
m.top.error = "Failed to request device authorization"
|
|
return
|
|
end if
|
|
|
|
deviceCode = deviceAuthResponse.device_code
|
|
userCode = deviceAuthResponse.user_code
|
|
verificationUriComplete = deviceAuthResponse.verification_uri_complete
|
|
expiresIn = deviceAuthResponse.expires_in
|
|
interval = deviceAuthResponse.interval
|
|
|
|
' Get verification URI for display - use user-friendly URL for prod only
|
|
verificationUri = deviceAuthResponse.verification_uri
|
|
apiConfigObj = ApiConfig()
|
|
if apiConfigObj.environment = "prod"
|
|
verificationUri = "floatplane.com/link"
|
|
end if
|
|
|
|
' Pass verification URI to main thread for QR code generation
|
|
' Note: verificationUriComplete is unchanged - QR code uses original API response
|
|
m.top.verificationUriComplete = verificationUriComplete
|
|
m.top.userCode = userCode
|
|
m.top.verificationUri = verificationUri
|
|
m.top.status = "QR_CODE_READY"
|
|
|
|
print "[OAUTH] OAuth device flow started"
|
|
print "[OAUTH] User code: " + userCode
|
|
|
|
' Poll for token
|
|
pollForToken(deviceCode, interval, expiresIn)
|
|
end sub
|
|
|
|
function requestDeviceAuthorization() as Object
|
|
appInfo = createObject("roAppInfo")
|
|
version = appInfo.getVersion()
|
|
useragent = "Hydravion (Roku) v" + version
|
|
|
|
apiConfigObj = ApiConfig()
|
|
url = apiConfigObj.buildRealmAuthUrl("openid-connect/auth/device")
|
|
https = CreateObject("roUrlTransfer")
|
|
https.RetainBodyOnError(true)
|
|
port = CreateObject("roMessagePort")
|
|
https.SetMessagePort(port)
|
|
https.SetUrl(url)
|
|
https.setCertificatesFile("common:/certs/ca-bundle.crt")
|
|
https.AddHeader("Content-Type", "application/x-www-form-urlencoded")
|
|
https.AddHeader("Accept", "application/json")
|
|
https.AddHeader("User-Agent", useragent)
|
|
https.initClientCertificates()
|
|
|
|
' Send client_id as form data
|
|
postData = "client_id=hydravion"
|
|
|
|
if https.AsyncPostFromString(postData)
|
|
while (true)
|
|
event = wait(10000, port)
|
|
if type(event) = "roUrlEvent"
|
|
code = event.GetResponseCode()
|
|
if code = 200
|
|
response = ParseJSON(event.GetString())
|
|
return response
|
|
else
|
|
responseBody = event.GetString()
|
|
print "[OAUTH] Device authorization error: " + code.ToStr() + " - " + responseBody
|
|
' Parse error to pass more specific information
|
|
errorResponse = ParseJSON(responseBody)
|
|
if errorResponse <> invalid and errorResponse.error <> invalid
|
|
' Pass the specific error type to the main thread
|
|
m.top.error = errorResponse.error
|
|
else
|
|
m.top.error = "Failed to request device authorization"
|
|
end if
|
|
return invalid
|
|
end if
|
|
else if event = invalid
|
|
https.AsyncCancel()
|
|
return invalid
|
|
end if
|
|
end while
|
|
end if
|
|
|
|
return invalid
|
|
end function
|
|
|
|
sub pollForToken(deviceCode as String, intervalSeconds as Integer, expiresInSeconds as Integer)
|
|
' Convert interval to milliseconds for sleep
|
|
intervalMs = intervalSeconds * 1000
|
|
if intervalMs < 1000
|
|
intervalMs = 1000 ' Minimum 1 second
|
|
end if
|
|
|
|
' Calculate expiration time
|
|
startTime = CreateObject("roDateTime")
|
|
startTimeSeconds = startTime.AsSeconds()
|
|
expirationTime = startTimeSeconds + expiresInSeconds
|
|
|
|
' Poll until token is received or expired
|
|
while (true)
|
|
' Check if expired
|
|
currentTime = CreateObject("roDateTime")
|
|
currentTimeSeconds = currentTime.AsSeconds()
|
|
if currentTimeSeconds >= expirationTime
|
|
m.top.error = "TIMEOUT"
|
|
m.top.status = "TIMEOUT"
|
|
print "[OAUTH] OAuth device flow expired"
|
|
return
|
|
end if
|
|
|
|
' Calculate remaining time
|
|
remainingSeconds = expirationTime - currentTimeSeconds
|
|
m.top.expiresIn = remainingSeconds.ToStr()
|
|
|
|
' Request token
|
|
tokenResponse = requestToken(deviceCode)
|
|
if tokenResponse <> invalid
|
|
' Success! Store tokens
|
|
registry = RegistryUtil()
|
|
registry.write("access_token", tokenResponse.access_token, "hydravion")
|
|
registry.write("refresh_token", tokenResponse.refresh_token, "hydravion")
|
|
|
|
' Store expiration time (current time + expires_in)
|
|
tokenExpirationTime = currentTimeSeconds + tokenResponse.expires_in
|
|
registry.write("token_expires_at", tokenExpirationTime.ToStr(), "hydravion")
|
|
|
|
' Store refresh token expiration
|
|
' If refresh_expires_in is 0, use 30-day idle limit (will be extended on each refresh)
|
|
if tokenResponse.refresh_expires_in <> invalid and tokenResponse.refresh_expires_in > 0
|
|
refreshExpirationTime = currentTimeSeconds + tokenResponse.refresh_expires_in
|
|
registry.write("refresh_token_expires_at", refreshExpirationTime.ToStr(), "hydravion")
|
|
refreshDays = tokenResponse.refresh_expires_in \ 86400
|
|
print "[OAUTH] Refresh token expires in " + refreshDays.ToStr() + " days (from API)"
|
|
else
|
|
refreshExpirationTime = currentTimeSeconds + (30 * 24 * 60 * 60) ' 30 days in seconds
|
|
registry.write("refresh_token_expires_at", refreshExpirationTime.ToStr(), "hydravion")
|
|
print "[OAUTH] Refresh token has 30-day idle limit (refresh_expires_in=0), expiration set to 30 days from now"
|
|
end if
|
|
|
|
m.top.status = "AUTHENTICATED"
|
|
print "[OAUTH] OAuth authentication successful"
|
|
return
|
|
end if
|
|
|
|
' Wait before next poll
|
|
sleep(intervalMs)
|
|
end while
|
|
end sub
|
|
|
|
function requestToken(deviceCode as String) as Object
|
|
appInfo = createObject("roAppInfo")
|
|
version = appInfo.getVersion()
|
|
useragent = "Hydravion (Roku) v" + version
|
|
|
|
apiConfigObj = ApiConfig()
|
|
url = apiConfigObj.buildRealmAuthUrl("openid-connect/token")
|
|
https = CreateObject("roUrlTransfer")
|
|
https.RetainBodyOnError(true)
|
|
port = CreateObject("roMessagePort")
|
|
https.SetMessagePort(port)
|
|
https.SetUrl(url)
|
|
https.setCertificatesFile("common:/certs/ca-bundle.crt")
|
|
https.AddHeader("Content-Type", "application/x-www-form-urlencoded")
|
|
https.AddHeader("Accept", "application/json")
|
|
https.AddHeader("User-Agent", useragent)
|
|
https.initClientCertificates()
|
|
|
|
' Send grant_type and device_code as form data
|
|
postData = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&client_id=hydravion&device_code=" + https.Escape(deviceCode)
|
|
|
|
if https.AsyncPostFromString(postData)
|
|
while (true)
|
|
event = wait(10000, port)
|
|
if type(event) = "roUrlEvent"
|
|
code = event.GetResponseCode()
|
|
if code = 200
|
|
response = ParseJSON(event.GetString())
|
|
return response
|
|
else if code = 400
|
|
' 400 might mean authorization_pending (user hasn't authorized yet)
|
|
' or expired_token, or other errors
|
|
responseBody = event.GetString()
|
|
errorResponse = ParseJSON(responseBody)
|
|
if errorResponse <> invalid and errorResponse.error <> invalid
|
|
if errorResponse.error = "authorization_pending"
|
|
' User hasn't authorized yet, continue polling
|
|
return invalid
|
|
else if errorResponse.error = "expired_token"
|
|
m.top.error = "EXPIRED"
|
|
m.top.status = "EXPIRED"
|
|
return invalid
|
|
else
|
|
' Other error
|
|
print "[OAUTH] Token request error: " + errorResponse.error
|
|
return invalid
|
|
end if
|
|
end if
|
|
return invalid
|
|
else
|
|
responseBody = event.GetString()
|
|
print "[OAUTH] Token request error: " + code.ToStr() + " - " + responseBody
|
|
return invalid
|
|
end if
|
|
else if event = invalid
|
|
https.AsyncCancel()
|
|
return invalid
|
|
end if
|
|
end while
|
|
end if
|
|
|
|
return invalid
|
|
end function
|
|
|