mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Add explicit HEAD handler for track stream that doesnt scrobble
This commit is contained in:
@@ -139,6 +139,29 @@ function server(
|
|||||||
</Presentation>`);
|
</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) => {
|
app.get("/stream/track/:id", async (req, res) => {
|
||||||
const id = req.params["id"]!;
|
const id = req.params["id"]!;
|
||||||
const accessToken = req.headers[BONOB_ACCESS_TOKEN_HEADER] as string;
|
const accessToken = req.headers[BONOB_ACCESS_TOKEN_HEADER] as string;
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ describe("server", () => {
|
|||||||
sid: 999,
|
sid: 999,
|
||||||
});
|
});
|
||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
(sonos as unknown) as Sonos,
|
sonos as unknown as Sonos,
|
||||||
theService,
|
theService,
|
||||||
"http://localhost:1234",
|
"http://localhost:1234",
|
||||||
new InMemoryMusicService()
|
new InMemoryMusicService()
|
||||||
@@ -199,16 +199,16 @@ describe("server", () => {
|
|||||||
};
|
};
|
||||||
const musicLibrary = {
|
const musicLibrary = {
|
||||||
stream: jest.fn(),
|
stream: jest.fn(),
|
||||||
scrobble: jest.fn()
|
scrobble: jest.fn(),
|
||||||
};
|
};
|
||||||
let now = dayjs();
|
let now = dayjs();
|
||||||
const accessTokens = new ExpiringAccessTokens({ now: () => now });
|
const accessTokens = new ExpiringAccessTokens({ now: () => now });
|
||||||
|
|
||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
(jest.fn() as unknown) as Sonos,
|
jest.fn() as unknown as Sonos,
|
||||||
aService(),
|
aService(),
|
||||||
"http://localhost:1234",
|
"http://localhost:1234",
|
||||||
(musicService as unknown) as MusicService,
|
musicService as unknown as MusicService,
|
||||||
new InMemoryLinkCodes(),
|
new InMemoryLinkCodes(),
|
||||||
accessTokens
|
accessTokens
|
||||||
);
|
);
|
||||||
@@ -221,6 +221,84 @@ describe("server", () => {
|
|||||||
accessToken = accessTokens.mint(authToken);
|
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", () => {
|
describe("when there is no access-token", () => {
|
||||||
it("should return a 401", async () => {
|
it("should return a 401", async () => {
|
||||||
const res = await request(server).get(`/stream/track/${trackId}`);
|
const res = await request(server).get(`/stream/track/${trackId}`);
|
||||||
@@ -250,8 +328,8 @@ describe("server", () => {
|
|||||||
"content-type": "audio/mp3",
|
"content-type": "audio/mp3",
|
||||||
},
|
},
|
||||||
stream: {
|
stream: {
|
||||||
pipe: (res: Response) => res.send("")
|
pipe: (res: Response) => res.send(""),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -275,8 +353,8 @@ describe("server", () => {
|
|||||||
"content-type": "audio/mp3",
|
"content-type": "audio/mp3",
|
||||||
},
|
},
|
||||||
stream: {
|
stream: {
|
||||||
pipe: (res: Response) => res.send("")
|
pipe: (res: Response) => res.send(""),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
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 sonos does not ask for a range", () => {
|
||||||
describe("when the music service does not return a content-range, content-length or accept-ranges", () => {
|
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 () => {
|
it("should return a 200 with the data, without adding the undefined headers", async () => {
|
||||||
|
const content = "some-track";
|
||||||
|
|
||||||
const trackStream = {
|
const trackStream = {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "audio/mp3",
|
"content-type": "audio/mp3",
|
||||||
|
// this content-length seems to be ignored for GET requests, stream.pipe must set its' own
|
||||||
|
"content-length": "666"
|
||||||
},
|
},
|
||||||
stream: {
|
stream: {
|
||||||
pipe: (res: Response) => res.send("")
|
pipe: (res: Response) => res.send(content),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -315,9 +420,14 @@ describe("server", () => {
|
|||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(trackStream.status);
|
expect(res.status).toEqual(trackStream.status);
|
||||||
expect(res.headers["content-type"]).toEqual("audio/mp3; charset=utf-8")
|
expect(res.headers["content-type"]).toEqual(
|
||||||
expect(Object.keys(res.headers)).not.toContain("content-range")
|
"audio/mp3; charset=utf-8"
|
||||||
expect(Object.keys(res.headers)).not.toContain("accept-ranges")
|
);
|
||||||
|
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,
|
"content-range": undefined,
|
||||||
},
|
},
|
||||||
stream: {
|
stream: {
|
||||||
pipe: (res: Response) => res.send("")
|
pipe: (res: Response) => res.send(""),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -345,9 +455,11 @@ describe("server", () => {
|
|||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(trackStream.status);
|
expect(res.status).toEqual(trackStream.status);
|
||||||
expect(res.headers["content-type"]).toEqual("audio/mp3; charset=utf-8")
|
expect(res.headers["content-type"]).toEqual(
|
||||||
expect(Object.keys(res.headers)).not.toContain("content-range")
|
"audio/mp3; charset=utf-8"
|
||||||
expect(Object.keys(res.headers)).not.toContain("accept-ranges")
|
);
|
||||||
|
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: {
|
stream: {
|
||||||
pipe: (res: Response) => {
|
pipe: (res: Response) => {
|
||||||
console.log("calling send on response")
|
console.log("calling send on response");
|
||||||
res.send("")
|
res.send("");
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -404,8 +516,8 @@ describe("server", () => {
|
|||||||
"content-range": "100-200",
|
"content-range": "100-200",
|
||||||
},
|
},
|
||||||
stream: {
|
stream: {
|
||||||
pipe: (res: Response) => res.send("")
|
pipe: (res: Response) => res.send(""),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -445,8 +557,8 @@ describe("server", () => {
|
|||||||
"content-range": "-100",
|
"content-range": "-100",
|
||||||
},
|
},
|
||||||
stream: {
|
stream: {
|
||||||
pipe: (res: Response) => res.send("")
|
pipe: (res: Response) => res.send(""),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -488,8 +600,8 @@ describe("server", () => {
|
|||||||
"content-range": "100-200",
|
"content-range": "100-200",
|
||||||
},
|
},
|
||||||
stream: {
|
stream: {
|
||||||
pipe: (res: Response) => res.send("")
|
pipe: (res: Response) => res.send(""),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -521,6 +633,7 @@ describe("server", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("art", () => {
|
describe("art", () => {
|
||||||
const musicService = {
|
const musicService = {
|
||||||
@@ -533,10 +646,10 @@ describe("server", () => {
|
|||||||
const accessTokens = new ExpiringAccessTokens({ now: () => now });
|
const accessTokens = new ExpiringAccessTokens({ now: () => now });
|
||||||
|
|
||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
(jest.fn() as unknown) as Sonos,
|
jest.fn() as unknown as Sonos,
|
||||||
aService(),
|
aService(),
|
||||||
"http://localhost:1234",
|
"http://localhost:1234",
|
||||||
(musicService as unknown) as MusicService,
|
musicService as unknown as MusicService,
|
||||||
new InMemoryLinkCodes(),
|
new InMemoryLinkCodes(),
|
||||||
accessTokens
|
accessTokens
|
||||||
);
|
);
|
||||||
@@ -605,7 +718,11 @@ describe("server", () => {
|
|||||||
expect(res.header["content-type"]).toEqual(coverArt.contentType);
|
expect(res.header["content-type"]).toEqual(coverArt.contentType);
|
||||||
|
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
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 () => {
|
it("should return a 500", async () => {
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
|
|
||||||
musicLibrary.coverArt.mockRejectedValue("Boom")
|
musicLibrary.coverArt.mockRejectedValue("Boom");
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
@@ -664,7 +781,11 @@ describe("server", () => {
|
|||||||
expect(res.header["content-type"]).toEqual(coverArt.contentType);
|
expect(res.header["content-type"]).toEqual(coverArt.contentType);
|
||||||
|
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
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", () => {
|
describe("when there is an error", () => {
|
||||||
it("should return a 500", async () => {
|
it("should return a 500", async () => {
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
musicLibrary.coverArt.mockRejectedValue("Boooooom")
|
musicLibrary.coverArt.mockRejectedValue("Boooooom");
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
@@ -700,5 +821,4 @@ describe("server", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user