From 419333399db19fe5709d27254dda58d6fc61991a Mon Sep 17 00:00:00 2001 From: chxx Date: Sun, 14 Sep 2025 23:52:37 +0800 Subject: [PATCH] update support 80.0.0 --- src/smapi.ts | 132 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 106 insertions(+), 26 deletions(-) diff --git a/src/smapi.ts b/src/smapi.ts index a4b0723..851b8f8 100644 --- a/src/smapi.ts +++ b/src/smapi.ts @@ -36,7 +36,9 @@ import { SmapiAuthTokens, SMAPI_FAULT_LOGIN_UNAUTHORIZED, ToSmapiFault, + SmapiToken, } from "./smapi_auth"; +import { IncomingHttpHeaders } from "http2"; export const LOGIN_ROUTE = "/login"; export const CREATE_REGISTRATION_ROUTE = "/registration/add"; @@ -161,17 +163,20 @@ class SonosSoap { bonobUrl: URLBuilder; smapiAuthTokens: SmapiAuthTokens; clock: Clock; + tokens: {[tokenKey:string]:SmapiToken}; constructor( bonobUrl: URLBuilder, linkCodes: LinkCodes, smapiAuthTokens: SmapiAuthTokens, clock: Clock + ) { this.bonobUrl = bonobUrl; this.linkCodes = linkCodes; this.smapiAuthTokens = smapiAuthTokens; this.clock = clock; + this.tokens = {}; } getAppLink(): GetAppLinkResult { @@ -233,6 +238,16 @@ class SonosSoap { }; } } + getCredentialsForToken(token: string): SmapiToken { + return this.tokens[token]!; + } + associateCredentialsForToken(token: string, fullSmapiToken: SmapiToken, oldToken?:string) { + logger.debug("Adding token: " + token + " " + JSON.stringify(fullSmapiToken)); + if(oldToken) { + delete this.tokens[oldToken]; + } + this.tokens[token] = fullSmapiToken; + } } export type ContainerType = "container" | "search" | "albumList"; @@ -415,7 +430,49 @@ function bindSmapiSoapServiceToExpress( ); }; - const login = async (credentials?: Credentials) => { + const swapToken = (expiredToken:string) => (newToken:SmapiToken) => { + logger.debug("oldToken: "+expiredToken); + logger.debug("newToken: "+JSON.stringify(newToken)); + sonosSoap.associateCredentialsForToken(newToken.token, newToken, expiredToken); + return TE.right(newToken); + } + + const useHeaderIfPresent = (credentials?: Credentials, headers?: IncomingHttpHeaders) => { + logger.debug("useHeaderIfPresent header", headers); + + const headersProvidedWithToken = headers!==null && headers!== undefined && headers["authorization"]; + if(headersProvidedWithToken) { + logger.debug("Will use authorization header"); + const bearer = headers["authorization"]; + const token = bearer?.split(" ")[1]; + if(token) { + const credsForToken = sonosSoap.getCredentialsForToken(token); + if(credsForToken==undefined) { + logger.debug("No creds for "+JSON.stringify(token)); + } else { + credentials = { + ...credentials!, + loginToken: { + ...credentials?.loginToken!, + token: credsForToken.token, + key: credsForToken.key, + } + } + logger.debug("Updated credentials to " + JSON.stringify(credentials)); + } + } + } + return credentials; + } + + const login = async (credentials?: Credentials, headers?: IncomingHttpHeaders) => { + + const credentialsProvidedWithoutAuthToken = credentials && credentials.loginToken.token==null; + if(credentialsProvidedWithoutAuthToken) { + credentials = useHeaderIfPresent(credentials, headers); + console.log("headers", headers) + console.log("credentials", credentials) + } const authOrFail = pipe( auth(credentials), E.getOrElseW((fault) => fault) @@ -431,6 +488,7 @@ function bindSmapiSoapServiceToExpress( throw await pipe( musicService.refreshToken(authOrFail.expiredToken), TE.map((it) => smapiAuthTokens.issue(it.serviceToken)), + TE.tap(swapToken(authOrFail.expiredToken)), TE.map((newToken) => ({ Fault: { faultcode: "Client.TokenRefreshRequired", @@ -457,8 +515,16 @@ function bindSmapiSoapServiceToExpress( Sonos: { SonosSoap: { getAppLink: () => sonosSoap.getAppLink(), - getDeviceAuthToken: ({ linkCode }: { linkCode: string }) => - sonosSoap.getDeviceAuthToken({ linkCode }), + getDeviceAuthToken: ({ linkCode }: { linkCode: string }) =>{ + const deviceAuthTokenResult = sonosSoap.getDeviceAuthToken({ linkCode }); + const smapiToken:SmapiToken = { + token: deviceAuthTokenResult.getDeviceAuthTokenResult.authToken, + key: deviceAuthTokenResult.getDeviceAuthTokenResult.privateKey + } + + sonosSoap.associateCredentialsForToken(smapiToken.token, smapiToken); + return deviceAuthTokenResult; + }, getLastUpdate: () => ({ getLastUpdateResult: { autoRefreshEnabled: true, @@ -467,9 +533,11 @@ function bindSmapiSoapServiceToExpress( pollInterval: 60, }, }), - refreshAuthToken: async (_, _2, soapyHeaders: SoapyHeaders) => { + refreshAuthToken: async (_, _2, soapyHeaders: SoapyHeaders, + { headers }: Pick) => { + const creds = useHeaderIfPresent(soapyHeaders?.credentials, headers); const serviceToken = pipe( - auth(soapyHeaders?.credentials), + auth(creds), E.fold( (fault) => isExpiredTokenError(fault) @@ -484,6 +552,7 @@ function bindSmapiSoapServiceToExpress( return pipe( musicService.refreshToken(serviceToken), TE.map((it) => smapiAuthTokens.issue(it.serviceToken)), + TE.tap(swapToken(serviceToken)), // ignores the return value, like a tee or peek TE.map((it) => ({ refreshAuthTokenResult: { authToken: it.token, @@ -498,9 +567,10 @@ function bindSmapiSoapServiceToExpress( getMediaURI: async ( { id }: { id: string }, _, - soapyHeaders: SoapyHeaders + soapyHeaders: SoapyHeaders, + { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(splitId(id)) .then(({ musicLibrary, credentials, type, typeId }) => { switch (type) { @@ -537,9 +607,10 @@ function bindSmapiSoapServiceToExpress( getMediaMetadata: async ( { id }: { id: string }, _, - soapyHeaders: SoapyHeaders + soapyHeaders: SoapyHeaders, + { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(splitId(id)) .then(async ({ musicLibrary, apiKey, type, typeId }) => { switch (type) { @@ -558,9 +629,10 @@ function bindSmapiSoapServiceToExpress( search: async ( { id, term }: { id: string; term: string }, _, - soapyHeaders: SoapyHeaders + soapyHeaders: SoapyHeaders, + { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(splitId(id)) .then(async ({ musicLibrary, apiKey }) => { switch (id) { @@ -603,9 +675,10 @@ function bindSmapiSoapServiceToExpress( }: // recursive, { id: string; index: number; count: number; recursive: boolean }, _, - soapyHeaders: SoapyHeaders + soapyHeaders: SoapyHeaders, + { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(splitId(id)) .then(async ({ musicLibrary, apiKey, type, typeId }) => { const paging = { _index: index, _count: count }; @@ -677,7 +750,7 @@ function bindSmapiSoapServiceToExpress( soapyHeaders: SoapyHeaders, { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(splitId(id)) .then(({ musicLibrary, apiKey, type, typeId }) => { const paging = { _index: index, _count: count }; @@ -685,6 +758,7 @@ function bindSmapiSoapServiceToExpress( logger.debug( `Fetching metadata type=${type}, typeId=${typeId}, acceptLanguage=${acceptLanguage}` ); + console.log("getMetadata headers", headers) const lang = i8n(...asLANGs(acceptLanguage)); const albums = (q: AlbumQuery): Promise => @@ -1000,9 +1074,10 @@ function bindSmapiSoapServiceToExpress( createContainer: async ( { title, seedId }: { title: string; seedId: string | undefined }, _, - soapyHeaders: SoapyHeaders + soapyHeaders: SoapyHeaders, + { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(({ musicLibrary }) => musicLibrary .createPlaylist(title) @@ -1026,17 +1101,19 @@ function bindSmapiSoapServiceToExpress( deleteContainer: async ( { id }: { id: string }, _, - soapyHeaders: SoapyHeaders + soapyHeaders: SoapyHeaders, + { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(({ musicLibrary }) => musicLibrary.deletePlaylist(id)) .then((_) => ({ deleteContainerResult: {} })), addToContainer: async ( { id, parentId }: { id: string; parentId: string }, _, - soapyHeaders: SoapyHeaders + soapyHeaders: SoapyHeaders, + { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(splitId(id)) .then(({ musicLibrary, typeId }) => musicLibrary.addToPlaylist(parentId.split(":")[1]!, typeId) @@ -1045,9 +1122,10 @@ function bindSmapiSoapServiceToExpress( removeFromContainer: async ( { id, indices }: { id: string; indices: string }, _, - soapyHeaders: SoapyHeaders + soapyHeaders: SoapyHeaders, + { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(splitId(id)) .then((it) => ({ ...it, @@ -1068,9 +1146,10 @@ function bindSmapiSoapServiceToExpress( rateItem: async ( { id, rating }: { id: string; rating: number }, _, - soapyHeaders: SoapyHeaders + soapyHeaders: SoapyHeaders, + { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(splitId(id)) .then(({ musicLibrary, typeId }) => musicLibrary.rate(typeId, ratingFromInt(Math.abs(rating))) @@ -1080,9 +1159,10 @@ function bindSmapiSoapServiceToExpress( setPlayedSeconds: async ( { id, seconds }: { id: string; seconds: string }, _, - soapyHeaders: SoapyHeaders + soapyHeaders: SoapyHeaders, + { headers }: Pick ) => - login(soapyHeaders?.credentials) + login(soapyHeaders?.credentials, headers) .then(splitId(id)) .then(({ musicLibrary, type, typeId }) => { switch (type) {