update support 80.0.0

This commit is contained in:
chxx
2025-09-14 23:52:37 +08:00
parent 2961b651d9
commit 419333399d

View File

@@ -36,7 +36,9 @@ import {
SmapiAuthTokens, SmapiAuthTokens,
SMAPI_FAULT_LOGIN_UNAUTHORIZED, SMAPI_FAULT_LOGIN_UNAUTHORIZED,
ToSmapiFault, ToSmapiFault,
SmapiToken,
} from "./smapi_auth"; } from "./smapi_auth";
import { IncomingHttpHeaders } from "http2";
export const LOGIN_ROUTE = "/login"; export const LOGIN_ROUTE = "/login";
export const CREATE_REGISTRATION_ROUTE = "/registration/add"; export const CREATE_REGISTRATION_ROUTE = "/registration/add";
@@ -161,17 +163,20 @@ class SonosSoap {
bonobUrl: URLBuilder; bonobUrl: URLBuilder;
smapiAuthTokens: SmapiAuthTokens; smapiAuthTokens: SmapiAuthTokens;
clock: Clock; clock: Clock;
tokens: {[tokenKey:string]:SmapiToken};
constructor( constructor(
bonobUrl: URLBuilder, bonobUrl: URLBuilder,
linkCodes: LinkCodes, linkCodes: LinkCodes,
smapiAuthTokens: SmapiAuthTokens, smapiAuthTokens: SmapiAuthTokens,
clock: Clock clock: Clock
) { ) {
this.bonobUrl = bonobUrl; this.bonobUrl = bonobUrl;
this.linkCodes = linkCodes; this.linkCodes = linkCodes;
this.smapiAuthTokens = smapiAuthTokens; this.smapiAuthTokens = smapiAuthTokens;
this.clock = clock; this.clock = clock;
this.tokens = {};
} }
getAppLink(): GetAppLinkResult { 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"; 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( const authOrFail = pipe(
auth(credentials), auth(credentials),
E.getOrElseW((fault) => fault) E.getOrElseW((fault) => fault)
@@ -431,6 +488,7 @@ function bindSmapiSoapServiceToExpress(
throw await pipe( throw await pipe(
musicService.refreshToken(authOrFail.expiredToken), musicService.refreshToken(authOrFail.expiredToken),
TE.map((it) => smapiAuthTokens.issue(it.serviceToken)), TE.map((it) => smapiAuthTokens.issue(it.serviceToken)),
TE.tap(swapToken(authOrFail.expiredToken)),
TE.map((newToken) => ({ TE.map((newToken) => ({
Fault: { Fault: {
faultcode: "Client.TokenRefreshRequired", faultcode: "Client.TokenRefreshRequired",
@@ -457,8 +515,16 @@ function bindSmapiSoapServiceToExpress(
Sonos: { Sonos: {
SonosSoap: { SonosSoap: {
getAppLink: () => sonosSoap.getAppLink(), getAppLink: () => sonosSoap.getAppLink(),
getDeviceAuthToken: ({ linkCode }: { linkCode: string }) => getDeviceAuthToken: ({ linkCode }: { linkCode: string }) =>{
sonosSoap.getDeviceAuthToken({ linkCode }), const deviceAuthTokenResult = sonosSoap.getDeviceAuthToken({ linkCode });
const smapiToken:SmapiToken = {
token: deviceAuthTokenResult.getDeviceAuthTokenResult.authToken,
key: deviceAuthTokenResult.getDeviceAuthTokenResult.privateKey
}
sonosSoap.associateCredentialsForToken(smapiToken.token, smapiToken);
return deviceAuthTokenResult;
},
getLastUpdate: () => ({ getLastUpdate: () => ({
getLastUpdateResult: { getLastUpdateResult: {
autoRefreshEnabled: true, autoRefreshEnabled: true,
@@ -467,9 +533,11 @@ function bindSmapiSoapServiceToExpress(
pollInterval: 60, pollInterval: 60,
}, },
}), }),
refreshAuthToken: async (_, _2, soapyHeaders: SoapyHeaders) => { refreshAuthToken: async (_, _2, soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">) => {
const creds = useHeaderIfPresent(soapyHeaders?.credentials, headers);
const serviceToken = pipe( const serviceToken = pipe(
auth(soapyHeaders?.credentials), auth(creds),
E.fold( E.fold(
(fault) => (fault) =>
isExpiredTokenError(fault) isExpiredTokenError(fault)
@@ -484,6 +552,7 @@ function bindSmapiSoapServiceToExpress(
return pipe( return pipe(
musicService.refreshToken(serviceToken), musicService.refreshToken(serviceToken),
TE.map((it) => smapiAuthTokens.issue(it.serviceToken)), TE.map((it) => smapiAuthTokens.issue(it.serviceToken)),
TE.tap(swapToken(serviceToken)), // ignores the return value, like a tee or peek
TE.map((it) => ({ TE.map((it) => ({
refreshAuthTokenResult: { refreshAuthTokenResult: {
authToken: it.token, authToken: it.token,
@@ -498,9 +567,10 @@ function bindSmapiSoapServiceToExpress(
getMediaURI: async ( getMediaURI: async (
{ id }: { id: string }, { id }: { id: string },
_, _,
soapyHeaders: SoapyHeaders soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(splitId(id)) .then(splitId(id))
.then(({ musicLibrary, credentials, type, typeId }) => { .then(({ musicLibrary, credentials, type, typeId }) => {
switch (type) { switch (type) {
@@ -537,9 +607,10 @@ function bindSmapiSoapServiceToExpress(
getMediaMetadata: async ( getMediaMetadata: async (
{ id }: { id: string }, { id }: { id: string },
_, _,
soapyHeaders: SoapyHeaders soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(splitId(id)) .then(splitId(id))
.then(async ({ musicLibrary, apiKey, type, typeId }) => { .then(async ({ musicLibrary, apiKey, type, typeId }) => {
switch (type) { switch (type) {
@@ -558,9 +629,10 @@ function bindSmapiSoapServiceToExpress(
search: async ( search: async (
{ id, term }: { id: string; term: string }, { id, term }: { id: string; term: string },
_, _,
soapyHeaders: SoapyHeaders soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(splitId(id)) .then(splitId(id))
.then(async ({ musicLibrary, apiKey }) => { .then(async ({ musicLibrary, apiKey }) => {
switch (id) { switch (id) {
@@ -603,9 +675,10 @@ function bindSmapiSoapServiceToExpress(
}: // recursive, }: // recursive,
{ id: string; index: number; count: number; recursive: boolean }, { id: string; index: number; count: number; recursive: boolean },
_, _,
soapyHeaders: SoapyHeaders soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(splitId(id)) .then(splitId(id))
.then(async ({ musicLibrary, apiKey, type, typeId }) => { .then(async ({ musicLibrary, apiKey, type, typeId }) => {
const paging = { _index: index, _count: count }; const paging = { _index: index, _count: count };
@@ -677,7 +750,7 @@ function bindSmapiSoapServiceToExpress(
soapyHeaders: SoapyHeaders, soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers"> { headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(splitId(id)) .then(splitId(id))
.then(({ musicLibrary, apiKey, type, typeId }) => { .then(({ musicLibrary, apiKey, type, typeId }) => {
const paging = { _index: index, _count: count }; const paging = { _index: index, _count: count };
@@ -685,6 +758,7 @@ function bindSmapiSoapServiceToExpress(
logger.debug( logger.debug(
`Fetching metadata type=${type}, typeId=${typeId}, acceptLanguage=${acceptLanguage}` `Fetching metadata type=${type}, typeId=${typeId}, acceptLanguage=${acceptLanguage}`
); );
console.log("getMetadata headers", headers)
const lang = i8n(...asLANGs(acceptLanguage)); const lang = i8n(...asLANGs(acceptLanguage));
const albums = (q: AlbumQuery): Promise<GetMetadataResponse> => const albums = (q: AlbumQuery): Promise<GetMetadataResponse> =>
@@ -1000,9 +1074,10 @@ function bindSmapiSoapServiceToExpress(
createContainer: async ( createContainer: async (
{ title, seedId }: { title: string; seedId: string | undefined }, { title, seedId }: { title: string; seedId: string | undefined },
_, _,
soapyHeaders: SoapyHeaders soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(({ musicLibrary }) => .then(({ musicLibrary }) =>
musicLibrary musicLibrary
.createPlaylist(title) .createPlaylist(title)
@@ -1026,17 +1101,19 @@ function bindSmapiSoapServiceToExpress(
deleteContainer: async ( deleteContainer: async (
{ id }: { id: string }, { id }: { id: string },
_, _,
soapyHeaders: SoapyHeaders soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(({ musicLibrary }) => musicLibrary.deletePlaylist(id)) .then(({ musicLibrary }) => musicLibrary.deletePlaylist(id))
.then((_) => ({ deleteContainerResult: {} })), .then((_) => ({ deleteContainerResult: {} })),
addToContainer: async ( addToContainer: async (
{ id, parentId }: { id: string; parentId: string }, { id, parentId }: { id: string; parentId: string },
_, _,
soapyHeaders: SoapyHeaders soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(splitId(id)) .then(splitId(id))
.then(({ musicLibrary, typeId }) => .then(({ musicLibrary, typeId }) =>
musicLibrary.addToPlaylist(parentId.split(":")[1]!, typeId) musicLibrary.addToPlaylist(parentId.split(":")[1]!, typeId)
@@ -1045,9 +1122,10 @@ function bindSmapiSoapServiceToExpress(
removeFromContainer: async ( removeFromContainer: async (
{ id, indices }: { id: string; indices: string }, { id, indices }: { id: string; indices: string },
_, _,
soapyHeaders: SoapyHeaders soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(splitId(id)) .then(splitId(id))
.then((it) => ({ .then((it) => ({
...it, ...it,
@@ -1068,9 +1146,10 @@ function bindSmapiSoapServiceToExpress(
rateItem: async ( rateItem: async (
{ id, rating }: { id: string; rating: number }, { id, rating }: { id: string; rating: number },
_, _,
soapyHeaders: SoapyHeaders soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(splitId(id)) .then(splitId(id))
.then(({ musicLibrary, typeId }) => .then(({ musicLibrary, typeId }) =>
musicLibrary.rate(typeId, ratingFromInt(Math.abs(rating))) musicLibrary.rate(typeId, ratingFromInt(Math.abs(rating)))
@@ -1080,9 +1159,10 @@ function bindSmapiSoapServiceToExpress(
setPlayedSeconds: async ( setPlayedSeconds: async (
{ id, seconds }: { id: string; seconds: string }, { id, seconds }: { id: string; seconds: string },
_, _,
soapyHeaders: SoapyHeaders soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, "headers">
) => ) =>
login(soapyHeaders?.credentials) login(soapyHeaders?.credentials, headers)
.then(splitId(id)) .then(splitId(id))
.then(({ musicLibrary, type, typeId }) => { .then(({ musicLibrary, type, typeId }) => {
switch (type) { switch (type) {