mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
update support 80.0.0
This commit is contained in:
132
src/smapi.ts
132
src/smapi.ts
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user