Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Interactive docs are served at `/` via [Scalar](https://scalar.com). The raw Ope
| GET | `/` | | Scalar API reference UI |
| GET | `/openapi.json` | | OpenAPI 3.0.3 spec |
| GET | `/album` | `slug` or (`artist`, `name`), `minimal` | Full album detail |
| GET | `/similar` | `slug` or (`artist`, `name`) | Similar albums |
| GET | `/releases` | `page` | New album releases |
| GET | `/releases/singles` | `page` | New single releases |
| GET | `/upcoming` | `page` | Upcoming releases |
Expand Down
19 changes: 19 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,25 @@ async function route(path: string, q: URLSearchParams, opts: FetchOpts): Promise
return detail;
}

if (path === "/similar") {
const slug = q.get("slug");
const artist = q.get("artist");
const name = q.get("name");

let albumPath: string;
if (slug) {
albumPath = `/album/${slug}/`;
} else if (artist && name) {
const url = await findAlbumUrl(artist, name, opts);
if (!url) throw new ApiError("Album not found", 404);
albumPath = new URL(url).pathname;
} else {
throw new ApiError("Provide either slug (ID or full slug) or both artist and name", 400);
}

return { albums: await fetchAlbumBlocks(`${albumPath}similar/`, opts) };
}

if (path === "/releases") {
const page = getPage(q);
return { page, albums: await fetchAlbumBlocks(`/releases/${page}/`, opts) };
Expand Down
51 changes: 51 additions & 0 deletions src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,57 @@ export const openApiSpec = {
},
},
},
"/similar": {
get: {
summary: "Similar albums",
description:
"Return the albums AOTY lists as similar to a given album (the 'You May Also Like' section). Provide either slug (ID or full slug for direct lookup) or both artist and name (search-based lookup).",
operationId: "getSimilar",
parameters: [
{ $ref: "#/components/parameters/CacheControl" },
{
name: "slug",
in: "query",
required: false,
schema: { type: "string" },
description: "AOTY album ID or full slug (e.g. '2915' or '2915-outkast-aquemini'). Use this or artist+name.",
example: "2915",
},
{
name: "artist",
in: "query",
required: false,
schema: { type: "string" },
example: "OutKast",
},
{
name: "name",
in: "query",
required: false,
schema: { type: "string" },
example: "Aquemini",
},
],
responses: {
"200": {
description: "List of similar albums",
content: {
"application/json": {
schema: {
type: "object",
properties: {
albums: { type: "array", items: { $ref: "#/components/schemas/AlbumBlock" } },
},
},
},
},
},
"400": { $ref: "#/components/responses/BadRequest" },
"404": { $ref: "#/components/responses/NotFound" },
"500": { $ref: "#/components/responses/ServerError" },
},
},
},
"/releases": {
get: {
summary: "New album releases",
Expand Down
18 changes: 18 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,24 @@ describe("GET /album parameter validation", () => {
});
});

describe("GET /similar parameter validation", () => {
it("returns 400 when both params missing", async () => {
const res = await fetch("/similar");
expect(res.status).toBe(400);
});

it("returns 400 when name missing", async () => {
const res = await fetch("/similar?artist=OutKast");
expect(res.status).toBe(400);
});

it("400 body has detail field (RFC 9457)", async () => {
const body = await (await fetch("/similar")).json() as { detail: string; status: number };
expect(body.detail).toContain("artist");
expect(body.status).toBe(400);
});
});

describe("GET /search parameter validation", () => {
const searchPaths = ["/search", "/search/albums", "/search/artists", "/search/labels"];

Expand Down