Files
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

309 lines
15 KiB
Plaintext

'********************************************************************
'** TokenUtil - OAuth Token Management
'********************************************************************
'** Testing token validity:
'** - Time-based: isTokenExpired() / isAuthenticated() use stored token_expires_at
'** (from server's expires_in). Fast, no network; does not detect server revocation.
'** - Server-based: Run validateTokenTask; when done, check task.isValid. True only
'** if the server returns 200 for an authenticated request (catches revocation).
'********************************************************************
function TokenUtil() as Object
tokenUtilObj = {
'** Get access token, refreshing if necessary
'@param skipRefresh
'@return access token string or invalid if not available
getAccessToken: function(skipRefresh = false as Boolean) as Dynamic
registry = RegistryUtil()
accessToken = registry.read("access_token", "hydravion")
if accessToken = invalid
print "[TOKEN] No access token found in registry"
return invalid
end if
' Check if token is expired (or about to expire)
if m.isTokenExpired()
if skipRefresh
' Called from render thread - return existing token, refresh will happen in Task
print "[TOKEN] Access token expired but skipping refresh (render thread), will refresh when used in Task"
return accessToken
end if
print "[TOKEN] Access token expired or expiring soon, attempting refresh..."
' Try to refresh
if m.refreshToken()
' Get the new token
newToken = registry.read("access_token", "hydravion")
print "[TOKEN] Successfully refreshed token"
return newToken
else
' Refresh failed, token invalid
print "[TOKEN] Token refresh failed, user must re-authenticate"
return invalid
end if
end if
' Log token status for debugging (only occasionally to avoid spam)
m.logTokenStatus()
return accessToken
end function
'** Check if access token is expired (time-based, no network)
' Uses token_expires_at stored from server's expires_in when token was issued/refreshed.
' For authoritative validity (e.g. detect revocation), use validateTokenTask instead.
'@return true if expired or missing, false if valid
isTokenExpired: function() as Boolean
registry = RegistryUtil()
expiresAt = registry.read("token_expires_at", "hydravion")
if expiresAt = invalid
print "[TOKEN] No expiration time found, token considered expired"
return true
end if
currentTime = CreateObject("roDateTime")
currentTimeSeconds = currentTime.AsSeconds()
expiresAtSeconds = Val(expiresAt)
refreshBufferSeconds = 300
timeUntilExpiration = expiresAtSeconds - currentTimeSeconds
if timeUntilExpiration <= refreshBufferSeconds
print "[TOKEN] Token expires in " + timeUntilExpiration.ToStr() + " seconds (buffer: " + refreshBufferSeconds.ToStr() + "), will refresh"
return true
end if
return false
end function
'** Refresh the access token using refresh token
'@return true if successful, false otherwise
refreshToken: function() as Boolean
registry = RegistryUtil()
refreshToken = registry.read("refresh_token", "hydravion")
if refreshToken = invalid
print "[TOKEN] No refresh token available"
return false
end if
refreshExpiresAt = registry.read("refresh_token_expires_at", "hydravion")
if refreshExpiresAt <> invalid
currentTime = CreateObject("roDateTime")
currentTimeSeconds = currentTime.AsSeconds()
refreshExpiresAtSeconds = Val(refreshExpiresAt)
if currentTimeSeconds >= refreshExpiresAtSeconds
print "[TOKEN] Stored refresh token expiration time has passed, but attempting refresh anyway (backend may still accept it)"
else
timeUntilRefreshExpiration = refreshExpiresAtSeconds - currentTimeSeconds
days = timeUntilRefreshExpiration \ 86400
hours = (timeUntilRefreshExpiration mod 86400) \ 3600
if days > 0
print "[TOKEN] Refresh token valid, expires in " + days.ToStr() + " days " + hours.ToStr() + " hours"
else
minutes = timeUntilRefreshExpiration \ 60
print "[TOKEN] Refresh token valid, expires in " + minutes.ToStr() + " minutes"
end if
end if
else
print "[TOKEN] No refresh token expiration time found (may be from old login), attempting refresh"
end if
' Request new token
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 refresh token request
postData = "grant_type=refresh_token&client_id=hydravion&refresh_token=" + https.Escape(refreshToken)
if https.AsyncPostFromString(postData)
while (true)
event = wait(10000, port)
if type(event) = "roUrlEvent"
code = event.GetResponseCode()
if code = 200
response = ParseJSON(event.GetString())
if response <> invalid and response.access_token <> invalid
' Store new tokens
registry.write("access_token", response.access_token, "hydravion")
' Update expiration time
currentTime = CreateObject("roDateTime")
currentTimeSeconds = currentTime.AsSeconds()
tokenExpirationTime = currentTimeSeconds + response.expires_in
registry.write("token_expires_at", tokenExpirationTime.ToStr(), "hydravion")
' Update refresh token if provided
if response.refresh_token <> invalid
registry.write("refresh_token", response.refresh_token, "hydravion")
end if
' Update refresh token expiration
if response.refresh_expires_in <> invalid and response.refresh_expires_in > 0
refreshExpirationTime = currentTimeSeconds + response.refresh_expires_in
registry.write("refresh_token_expires_at", refreshExpirationTime.ToStr(), "hydravion")
refreshDays = response.refresh_expires_in \ 86400
refreshHours = (response.refresh_expires_in mod 86400) \ 3600
if refreshDays > 0
print "[TOKEN] Refresh token expires in " + refreshDays.ToStr() + " days (from API)"
else
refreshMinutes = response.refresh_expires_in \ 60
print "[TOKEN] Refresh token expires in " + refreshMinutes.ToStr() + " minutes (from API)"
end if
else
refreshExpirationTime = currentTimeSeconds + (30 * 24 * 60 * 60) ' 30 days in seconds
registry.write("refresh_token_expires_at", refreshExpirationTime.ToStr(), "hydravion")
print "[TOKEN] Refresh token has 30-day idle limit (refresh_expires_in=0), extending expiration by 30 days from now"
end if
accessMinutes = response.expires_in \ 60
print "[TOKEN] Token refreshed successfully, new access token expires in " + accessMinutes.ToStr() + " minutes"
return true
else
print "[TOKEN] Invalid refresh response"
m.clearTokens()
return false
end if
else
responseBody = event.GetString()
print "[TOKEN] Token refresh error: " + code.ToStr() + " - " + responseBody
' If refresh fails, clear tokens (likely expired or invalid)
m.clearTokens()
return false
end if
else if event = invalid
https.AsyncCancel()
m.clearTokens()
return false
end if
end while
end if
return false
end function
'** Clear all stored tokens
clearTokens: function() as Void
registry = RegistryUtil()
registry.delete("access_token", "hydravion")
registry.delete("refresh_token", "hydravion")
registry.delete("token_expires_at", "hydravion")
registry.delete("refresh_token_expires_at", "hydravion")
' Also clear old sails cookie if it exists
registry.delete("sails", "hydravion")
end function
'** Check if user is authenticated (has valid token)
'@return true if authenticated, false otherwise
' Note: Does not attempt refresh (to avoid render thread issues)
' Actual refresh will happen when token is used in Task context
isAuthenticated: function() as Boolean
registry = RegistryUtil()
accessToken = registry.read("access_token", "hydravion")
if accessToken = invalid
return false
end if
' Check if token is expired (but don't refresh here - that happens in Task context)
if m.isTokenExpired()
' Token exists but is expired - still consider authenticated
' Refresh will happen when token is actually used in a Task
return true
end if
return true
end function
'** Log token status for debugging/testing
' Only logs occasionally to avoid spam
logTokenStatus: function() as Void
' Use a simple counter to log only every 10th call
if m.logCounter = invalid
m.logCounter = 0
end if
m.logCounter = m.logCounter + 1
' Log every 10th call (roughly every 10 API calls)
if m.logCounter mod 10 = 0
registry = RegistryUtil()
expiresAt = registry.read("token_expires_at", "hydravion")
if expiresAt <> invalid
currentTime = CreateObject("roDateTime")
currentTimeSeconds = currentTime.AsSeconds()
expiresAtSeconds = Val(expiresAt)
timeUntilExpiration = expiresAtSeconds - currentTimeSeconds
minutes = timeUntilExpiration \ 60
seconds = timeUntilExpiration mod 60
print "[TOKEN] Token valid, expires in " + minutes.ToStr() + "m " + seconds.ToStr() + "s"
end if
end if
end function
'** Get token status for debugging/testing
'@return object with token status information
getTokenStatus: function() as Object
registry = RegistryUtil()
status = {
hasAccessToken: false
hasRefreshToken: false
accessTokenExpiresAt: invalid
refreshTokenExpiresAt: invalid
timeUntilAccessExpiration: invalid
timeUntilRefreshExpiration: invalid
isAccessTokenExpired: false
isRefreshTokenExpired: false
}
accessToken = registry.read("access_token", "hydravion")
status.hasAccessToken = (accessToken <> invalid)
refreshToken = registry.read("refresh_token", "hydravion")
status.hasRefreshToken = (refreshToken <> invalid)
expiresAt = registry.read("token_expires_at", "hydravion")
if expiresAt <> invalid
status.accessTokenExpiresAt = expiresAt
currentTime = CreateObject("roDateTime")
currentTimeSeconds = currentTime.AsSeconds()
expiresAtSeconds = Val(expiresAt)
status.timeUntilAccessExpiration = expiresAtSeconds - currentTimeSeconds
status.isAccessTokenExpired = m.isTokenExpired()
end if
refreshExpiresAt = registry.read("refresh_token_expires_at", "hydravion")
if refreshExpiresAt <> invalid
status.refreshTokenExpiresAt = refreshExpiresAt
currentTime = CreateObject("roDateTime")
currentTimeSeconds = currentTime.AsSeconds()
refreshExpiresAtSeconds = Val(refreshExpiresAt)
status.timeUntilRefreshExpiration = refreshExpiresAtSeconds - currentTimeSeconds
status.isRefreshTokenExpired = (currentTimeSeconds >= refreshExpiresAtSeconds)
end if
return status
end function
}
return tokenUtilObj
end function