Add explicit HEAD handler for track stream that doesnt scrobble

This commit is contained in:
simojenki
2021-06-19 12:26:49 +10:00
parent 79c8c99c1b
commit e6378de25d
2 changed files with 430 additions and 287 deletions

View File

@@ -139,6 +139,29 @@ function server(
</Presentation>`);
});
app.head("/stream/track/:id", async (req, res) => {
const id = req.params["id"]!;
const accessToken = req.headers[BONOB_ACCESS_TOKEN_HEADER] as string;
const authToken = accessTokens.authTokenFor(accessToken);
if (!authToken) {
return res.status(401).send();
} else {
return musicService
.login(authToken)
.then((it) =>
it.stream({ trackId: id, range: req.headers["range"] || undefined })
)
.then((trackStream) => {
res.status(trackStream.status);
Object.entries(trackStream.headers)
.filter(([_, v]) => v !== undefined)
.forEach(([header, value]) => res.setHeader(header, value));
res.send();
});
}
});
app.get("/stream/track/:id", async (req, res) => {
const id = req.params["id"]!;
const accessToken = req.headers[BONOB_ACCESS_TOKEN_HEADER] as string;

View File

@@ -158,7 +158,7 @@ describe("server", () => {
sid: 999,
});
const server = makeServer(
(sonos as unknown) as Sonos,
sonos as unknown as Sonos,
theService,
"http://localhost:1234",
new InMemoryMusicService()
@@ -199,16 +199,16 @@ describe("server", () => {
};
const musicLibrary = {
stream: jest.fn(),
scrobble: jest.fn()
scrobble: jest.fn(),
};
let now = dayjs();
const accessTokens = new ExpiringAccessTokens({ now: () => now });
const server = makeServer(
(jest.fn() as unknown) as Sonos,
jest.fn() as unknown as Sonos,
aService(),
"http://localhost:1234",
(musicService as unknown) as MusicService,
musicService as unknown as MusicService,
new InMemoryLinkCodes(),
accessTokens
);
@@ -221,6 +221,84 @@ describe("server", () => {
accessToken = accessTokens.mint(authToken);
});
describe("HEAD requests", () => {
describe("when there is no access-token", () => {
it("should return a 401", async () => {
const res = await request(server).head(`/stream/track/${trackId}`);
expect(res.status).toEqual(401);
});
});
describe("when the access-token has expired", () => {
it("should return a 401", async () => {
now = now.add(1, "day");
const res = await request(server)
.head(`/stream/track/${trackId}`)
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
expect(res.status).toEqual(401);
});
});
describe("when the access-token is valid", () => {
describe("and the track exists", () => {
it("should return a 200", async () => {
const trackStream = {
status: 200,
headers: {
"content-type": "audio/mp3; charset=utf-8",
"content-length": "123",
},
stream: {
pipe: (res: Response) => res.send(""),
},
};
musicService.login.mockResolvedValue(musicLibrary);
musicLibrary.stream.mockResolvedValue(trackStream);
const res = await request(server)
.head(`/stream/track/${trackId}`)
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
expect(res.status).toEqual(trackStream.status);
expect(res.headers["content-type"]).toEqual(
"audio/mp3; charset=utf-8"
);
expect(res.headers["content-length"]).toEqual(
"123"
);
expect(res.body).toEqual({});
});
});
describe("and the track doesnt exist", () => {
it("should return a 404", async () => {
const trackStream = {
status: 404,
headers: {},
stream: {
pipe: (res: Response) => res.send(""),
},
};
musicService.login.mockResolvedValue(musicLibrary);
musicLibrary.stream.mockResolvedValue(trackStream);
const res = await request(server)
.head(`/stream/track/${trackId}`)
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
expect(res.status).toEqual(404);
expect(res.body).toEqual({});
});
});
});
});
describe("GET requests", () => {
describe("when there is no access-token", () => {
it("should return a 401", async () => {
const res = await request(server).get(`/stream/track/${trackId}`);
@@ -250,8 +328,8 @@ describe("server", () => {
"content-type": "audio/mp3",
},
stream: {
pipe: (res: Response) => res.send("")
}
pipe: (res: Response) => res.send(""),
},
};
musicService.login.mockResolvedValue(musicLibrary);
@@ -275,8 +353,8 @@ describe("server", () => {
"content-type": "audio/mp3",
},
stream: {
pipe: (res: Response) => res.send("")
}
pipe: (res: Response) => res.send(""),
},
};
musicService.login.mockResolvedValue(musicLibrary);
@@ -293,17 +371,44 @@ describe("server", () => {
});
});
describe("when the track doesnt exist", () => {
it("should return a 404", async () => {
const trackStream = {
status: 404,
headers: {
},
stream: {
pipe: (res: Response) => res.send(""),
},
};
musicService.login.mockResolvedValue(musicLibrary);
musicLibrary.stream.mockResolvedValue(trackStream);
musicLibrary.scrobble.mockResolvedValue(false);
const res = await request(server)
.get(`/stream/track/${trackId}`)
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
expect(res.status).toEqual(404);
});
});
describe("when sonos does not ask for a range", () => {
describe("when the music service does not return a content-range, content-length or accept-ranges", () => {
it("should return a 200 with the data, without adding the undefined headers", async () => {
const content = "some-track";
const trackStream = {
status: 200,
headers: {
"content-type": "audio/mp3",
// this content-length seems to be ignored for GET requests, stream.pipe must set its' own
"content-length": "666"
},
stream: {
pipe: (res: Response) => res.send("")
}
pipe: (res: Response) => res.send(content),
},
};
musicService.login.mockResolvedValue(musicLibrary);
@@ -315,9 +420,14 @@ describe("server", () => {
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
expect(res.status).toEqual(trackStream.status);
expect(res.headers["content-type"]).toEqual("audio/mp3; charset=utf-8")
expect(Object.keys(res.headers)).not.toContain("content-range")
expect(Object.keys(res.headers)).not.toContain("accept-ranges")
expect(res.headers["content-type"]).toEqual(
"audio/mp3; charset=utf-8"
);
expect(res.headers["content-length"]).toEqual(
`${content.length}`
);
expect(Object.keys(res.headers)).not.toContain("content-range");
expect(Object.keys(res.headers)).not.toContain("accept-ranges");
});
});
@@ -332,8 +442,8 @@ describe("server", () => {
"content-range": undefined,
},
stream: {
pipe: (res: Response) => res.send("")
}
pipe: (res: Response) => res.send(""),
},
};
musicService.login.mockResolvedValue(musicLibrary);
@@ -345,9 +455,11 @@ describe("server", () => {
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
expect(res.status).toEqual(trackStream.status);
expect(res.headers["content-type"]).toEqual("audio/mp3; charset=utf-8")
expect(Object.keys(res.headers)).not.toContain("content-range")
expect(Object.keys(res.headers)).not.toContain("accept-ranges")
expect(res.headers["content-type"]).toEqual(
"audio/mp3; charset=utf-8"
);
expect(Object.keys(res.headers)).not.toContain("content-range");
expect(Object.keys(res.headers)).not.toContain("accept-ranges");
});
});
@@ -363,10 +475,10 @@ describe("server", () => {
},
stream: {
pipe: (res: Response) => {
console.log("calling send on response")
res.send("")
}
}
console.log("calling send on response");
res.send("");
},
},
};
musicService.login.mockResolvedValue(musicLibrary);
@@ -404,8 +516,8 @@ describe("server", () => {
"content-range": "100-200",
},
stream: {
pipe: (res: Response) => res.send("")
}
pipe: (res: Response) => res.send(""),
},
};
musicService.login.mockResolvedValue(musicLibrary);
@@ -445,8 +557,8 @@ describe("server", () => {
"content-range": "-100",
},
stream: {
pipe: (res: Response) => res.send("")
}
pipe: (res: Response) => res.send(""),
},
};
musicService.login.mockResolvedValue(musicLibrary);
@@ -488,8 +600,8 @@ describe("server", () => {
"content-range": "100-200",
},
stream: {
pipe: (res: Response) => res.send("")
}
pipe: (res: Response) => res.send(""),
},
};
musicService.login.mockResolvedValue(musicLibrary);
@@ -521,6 +633,7 @@ describe("server", () => {
});
});
});
});
describe("art", () => {
const musicService = {
@@ -533,10 +646,10 @@ describe("server", () => {
const accessTokens = new ExpiringAccessTokens({ now: () => now });
const server = makeServer(
(jest.fn() as unknown) as Sonos,
jest.fn() as unknown as Sonos,
aService(),
"http://localhost:1234",
(musicService as unknown) as MusicService,
musicService as unknown as MusicService,
new InMemoryLinkCodes(),
accessTokens
);
@@ -605,7 +718,11 @@ describe("server", () => {
expect(res.header["content-type"]).toEqual(coverArt.contentType);
expect(musicService.login).toHaveBeenCalledWith(authToken);
expect(musicLibrary.coverArt).toHaveBeenCalledWith(albumId, "artist", 180);
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
albumId,
"artist",
180
);
});
});
@@ -629,7 +746,7 @@ describe("server", () => {
it("should return a 500", async () => {
musicService.login.mockResolvedValue(musicLibrary);
musicLibrary.coverArt.mockRejectedValue("Boom")
musicLibrary.coverArt.mockRejectedValue("Boom");
const res = await request(server)
.get(
@@ -664,7 +781,11 @@ describe("server", () => {
expect(res.header["content-type"]).toEqual(coverArt.contentType);
expect(musicService.login).toHaveBeenCalledWith(authToken);
expect(musicLibrary.coverArt).toHaveBeenCalledWith(albumId, "album", 180);
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
albumId,
"album",
180
);
});
});
@@ -686,7 +807,7 @@ describe("server", () => {
describe("when there is an error", () => {
it("should return a 500", async () => {
musicService.login.mockResolvedValue(musicLibrary);
musicLibrary.coverArt.mockRejectedValue("Boooooom")
musicLibrary.coverArt.mockRejectedValue("Boooooom");
const res = await request(server)
.get(
@@ -700,5 +821,4 @@ describe("server", () => {
});
});
});
});