@misofm/sdk 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/catalog.d.ts +4 -17
  2. package/dist/catalog.d.ts.map +1 -1
  3. package/dist/catalog.js +46 -58
  4. package/dist/catalog.js.map +1 -1
  5. package/dist/cover.d.ts +10 -0
  6. package/dist/cover.d.ts.map +1 -1
  7. package/dist/cover.js +66 -19
  8. package/dist/cover.js.map +1 -1
  9. package/dist/credits.d.ts +10 -0
  10. package/dist/credits.d.ts.map +1 -1
  11. package/dist/credits.js +149 -39
  12. package/dist/credits.js.map +1 -1
  13. package/dist/drop.d.ts +7 -0
  14. package/dist/drop.d.ts.map +1 -1
  15. package/dist/drop.js +84 -34
  16. package/dist/drop.js.map +1 -1
  17. package/dist/pressing.d.ts.map +1 -1
  18. package/dist/pressing.js +59 -16
  19. package/dist/pressing.js.map +1 -1
  20. package/dist/read/artist.d.ts +1 -6
  21. package/dist/read/artist.d.ts.map +1 -1
  22. package/dist/read/artist.js +39 -22
  23. package/dist/read/artist.js.map +1 -1
  24. package/dist/read/catalog.d.ts +24 -3
  25. package/dist/read/catalog.d.ts.map +1 -1
  26. package/dist/read/catalog.js +190 -89
  27. package/dist/read/catalog.js.map +1 -1
  28. package/dist/read/index.d.ts +5 -3
  29. package/dist/read/index.d.ts.map +1 -1
  30. package/dist/read/index.js +2 -2
  31. package/dist/read/index.js.map +1 -1
  32. package/dist/read/types.d.ts +12 -1
  33. package/dist/read/types.d.ts.map +1 -1
  34. package/dist/read/works.d.ts +3 -2
  35. package/dist/read/works.d.ts.map +1 -1
  36. package/dist/read/works.js +158 -48
  37. package/dist/read/works.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/catalog.ts +71 -77
  40. package/src/cover.ts +107 -26
  41. package/src/credits.ts +353 -89
  42. package/src/drop.ts +150 -40
  43. package/src/pressing.ts +146 -36
  44. package/src/read/artist.ts +73 -34
  45. package/src/read/catalog.ts +324 -89
  46. package/src/read/index.ts +23 -2
  47. package/src/read/types.ts +16 -2
  48. package/src/read/works.ts +224 -59
@@ -10,13 +10,25 @@
10
10
  // credits → recordings); the same work happens once here, inside one datacenter,
11
11
  // and every subsequent visitor is served from cache.
12
12
 
13
- import { getDrop, getCurrentDrop, type DropView } from "../drop.ts";
14
- import { getReleaseCover, type ReleaseCoverView, type CoverImageRef } from "../cover.ts";
15
- import { getReleaseCredits, getRecordingCredits, type CreditView } from "../credits.ts";
16
- import { getReleaseById, isNotFound } from "@misonetwork/sdk";
13
+ import { getCurrentDrops, getDrop, type DropView } from "../drop.ts";
14
+ import {
15
+ getReleaseCoversByIds,
16
+ parseReleaseCoverContent,
17
+ releaseCoverFieldId,
18
+ type ReleaseCoverView,
19
+ type CoverImageRef,
20
+ } from "../cover.ts";
21
+ import {
22
+ getReleaseCreditsByIds,
23
+ parseReleaseCreditsContent,
24
+ releaseCreditsFieldId,
25
+ type CreditView,
26
+ } from "../credits.ts";
27
+ import { getTrackCreditsByRecordingIds } from "../catalog.ts";
28
+ import { getReleaseById, getReleasesByIds, isNotFound } from "@misonetwork/sdk";
17
29
  import type { Release } from "@misonetwork/sdk";
18
30
  import type { MisoClient } from "./client.ts";
19
- import { getRecordingTitles } from "./works.ts";
31
+ import { getRecordingTitles, parseReleaseObject } from "./works.ts";
20
32
  import { gte, int, ms, msOrNull, u64, u64OrNull } from "./internal/scalars.ts";
21
33
  import { quiltPatchId, u256ToB64Url } from "./internal/walrus.ts";
22
34
  import type {
@@ -41,15 +53,30 @@ import type {
41
53
  /** Aggregator URL for a cover image ref, whichever Walrus variant it is. */
42
54
  function imageUrl(aggregator: string, ref: CoverImageRef): string {
43
55
  const base = aggregator.replace(/\/$/, "");
44
- if (ref.kind === "blob") return `${base}/v1/blobs/${u256ToB64Url(ref.blobId)}`;
45
- const patch = quiltPatchId(ref.quiltId, ref.version, ref.startIndex, ref.endIndex);
56
+ if (ref.kind === "blob")
57
+ return `${base}/v1/blobs/${u256ToB64Url(ref.blobId)}`;
58
+ const patch = quiltPatchId(
59
+ ref.quiltId,
60
+ ref.version,
61
+ ref.startIndex,
62
+ ref.endIndex,
63
+ );
46
64
  return `${base}/v1/blobs/by-quilt-patch-id/${patch}`;
47
65
  }
48
66
 
49
- function toCover(aggregator: string, view: ReleaseCoverView | null): Cover | null {
67
+ function toCover(
68
+ aggregator: string,
69
+ view: ReleaseCoverView | null,
70
+ ): Cover | null {
50
71
  if (!view) return null;
51
- const image = (ref: CoverImageRef): CoverImage => ({ kind: ref.kind, url: imageUrl(aggregator, ref) });
52
- return { still: image(view.still), animated: view.animated ? image(view.animated) : null };
72
+ const image = (ref: CoverImageRef): CoverImage => ({
73
+ kind: ref.kind,
74
+ url: imageUrl(aggregator, ref),
75
+ });
76
+ return {
77
+ still: image(view.still),
78
+ animated: view.animated ? image(view.animated) : null,
79
+ };
53
80
  }
54
81
 
55
82
  // ── Currency ─────────────────────────────────────────────────────────────────
@@ -61,9 +88,14 @@ function toCover(aggregator: string, view: ReleaseCoverView | null): Cover | nul
61
88
  */
62
89
  export function currencyInfo(type: string | null): Currency {
63
90
  if (!type) return { type: null, symbol: "COIN", decimals: 9 };
64
- if (/::fakeusd::/i.test(type)) return { type, symbol: "FAKEUSD", decimals: 6 };
91
+ if (/::fakeusd::/i.test(type))
92
+ return { type, symbol: "FAKEUSD", decimals: 6 };
65
93
  if (type === "0x2::sui::SUI") return { type, symbol: "SUI", decimals: 9 };
66
- return { type, symbol: (type.split("::").pop() ?? "COIN").toUpperCase(), decimals: 9 };
94
+ return {
95
+ type,
96
+ symbol: (type.split("::").pop() ?? "COIN").toUpperCase(),
97
+ decimals: 9,
98
+ };
67
99
  }
68
100
 
69
101
  // ── Projections ──────────────────────────────────────────────────────────────
@@ -71,7 +103,10 @@ export function currencyInfo(type: string | null): Currency {
71
103
  function toPressingView(drop: DropView): PressingView {
72
104
  const quantitySold = u64(drop.quantitySold);
73
105
  const maxSupply = u64OrNull(drop.maxSupply);
74
- const price: Price = { kind: drop.price.kind, amount: u64(drop.price.amount) };
106
+ const price: Price = {
107
+ kind: drop.price.kind,
108
+ amount: u64(drop.price.amount),
109
+ };
75
110
  return {
76
111
  id: drop.id,
77
112
  releaseId: drop.releaseId,
@@ -87,29 +122,40 @@ function toPressingView(drop: DropView): PressingView {
87
122
  }
88
123
 
89
124
  function toWorkState(state: Release["state"]): WorkState {
90
- return state.type === "Published" ? { type: "Published", timestampMs: state.timestampMs } : { type: "Initialized" };
125
+ return state.type === "Published"
126
+ ? { type: "Published", timestampMs: state.timestampMs }
127
+ : { type: "Initialized" };
91
128
  }
92
129
 
93
130
  function toCredits(credits: CreditView[] | null): Credit[] {
94
- return (credits ?? []).map((c) => ({ partyId: c.partyId, displayName: c.displayName, roles: [...c.roles] }));
131
+ return (credits ?? []).map((c) => ({
132
+ partyId: c.partyId,
133
+ displayName: c.displayName,
134
+ roles: [...c.roles],
135
+ }));
95
136
  }
96
137
 
97
138
  /** The release's PRIMARY credits, in chain order — a release's artist line. */
98
139
  export function primaryArtistNames(credits: Credit[]): string[] {
99
- return credits.filter((c) => c.roles.includes("Primary")).map((c) => c.displayName);
140
+ return credits
141
+ .filter((c) => c.roles.includes("Primary"))
142
+ .map((c) => c.displayName);
100
143
  }
101
144
 
102
145
  /**
103
146
  * Number the protocol's flat tracklist. Display grouping such as discs belongs
104
147
  * to metadata extensions and can be layered onto this projection later.
105
148
  */
106
- function toTracks(release: Release, titles: Record<string, string>): TrackView[] {
149
+ function toTracks(
150
+ release: Release,
151
+ titles: Record<string, string>,
152
+ ): TrackView[] {
107
153
  return release.tracks.map((track, index) => ({
108
- no: `${index + 1}`,
109
- title: titles[track.recordingId] ?? "Untitled",
110
- recordingId: track.recordingId,
111
- splitBps: int(track.splitBps.value),
112
- disc: 1,
154
+ no: `${index + 1}`,
155
+ title: titles[track.recordingId] ?? "Untitled",
156
+ recordingId: track.recordingId,
157
+ splitBps: int(track.splitBps.value),
158
+ disc: 1,
113
159
  }));
114
160
  }
115
161
 
@@ -124,13 +170,105 @@ function toTracks(release: Release, titles: Record<string, string>): TrackView[]
124
170
  * simply misses them rather than failing. Ported verbatim from miso-app's
125
171
  * `lib/pressing.ts:readReleaseCover`.
126
172
  */
127
- export async function readReleaseCover(client: MisoClient, releaseId: string): Promise<Cover | null> {
173
+ export async function readReleaseCover(
174
+ client: MisoClient,
175
+ releaseId: string,
176
+ ): Promise<Cover | null> {
128
177
  const { releaseCoverArt, legacyReleaseCoverArt } = client.config.protocol;
129
- for (const pkg of [releaseCoverArt, ...legacyReleaseCoverArt]) {
130
- const cover = await getReleaseCover(client.protocol, releaseId, pkg).catch(() => null);
131
- if (cover) return toCover(client.config.walrusAggregatorUrl, cover);
178
+ const covers = await getReleaseCoversByIds(
179
+ client.protocol,
180
+ [releaseId],
181
+ [releaseCoverArt, ...legacyReleaseCoverArt],
182
+ ).catch(() => ({}) as Partial<Record<string, ReleaseCoverView>>);
183
+ return toCover(client.config.walrusAggregatorUrl, covers[releaseId] ?? null);
184
+ }
185
+
186
+ export type ReleaseResourceInclude = "cover" | "credits";
187
+
188
+ export interface ReleaseResources {
189
+ release: Release;
190
+ cover?: Cover | null;
191
+ credits?: Credit[];
192
+ }
193
+
194
+ /**
195
+ * A release plus selected derived resources in one heterogeneous Core batch.
196
+ * Every extension id is deterministic, so separate object calls only add
197
+ * network round trips without discovering anything new.
198
+ */
199
+ export async function getReleaseResources(
200
+ client: MisoClient,
201
+ releaseId: string,
202
+ include: readonly ReleaseResourceInclude[] = [],
203
+ ): Promise<ReleaseResources> {
204
+ const wantsCover = include.includes("cover");
205
+ const wantsCredits = include.includes("credits");
206
+ const coverPackages = wantsCover
207
+ ? [
208
+ client.config.protocol.releaseCoverArt,
209
+ ...client.config.protocol.legacyReleaseCoverArt,
210
+ ]
211
+ : [];
212
+ const coverFieldIds = coverPackages.map((packageId) =>
213
+ releaseCoverFieldId(releaseId, packageId),
214
+ );
215
+ const creditsFieldId = wantsCredits
216
+ ? releaseCreditsFieldId(releaseId, client.config.protocol.releaseCredits)
217
+ : null;
218
+ const objectIds = [
219
+ releaseId,
220
+ ...coverFieldIds,
221
+ ...(creditsFieldId ? [creditsFieldId] : []),
222
+ ];
223
+ const { objects } = await client.protocol.core.getObjects({
224
+ objectIds,
225
+ include: { content: true, json: true },
226
+ });
227
+ const releaseObject = objects[0];
228
+ if (!releaseObject) throw new Error(`Release not found: ${releaseId}`);
229
+ if (releaseObject instanceof Error) throw releaseObject;
230
+ if (!releaseObject.content)
231
+ throw new Error(`Release has no content: ${releaseId}`);
232
+ const release = parseReleaseObject(
233
+ releaseObject.objectId,
234
+ releaseObject.content,
235
+ releaseObject.json,
236
+ );
237
+
238
+ let cover: Cover | null | undefined;
239
+ if (wantsCover) {
240
+ let view: ReleaseCoverView | null = null;
241
+ for (let index = 0; index < coverFieldIds.length; index += 1) {
242
+ const object = objects[index + 1];
243
+ if (!object || object instanceof Error || !object.content) continue;
244
+ try {
245
+ view = parseReleaseCoverContent(object.content);
246
+ } catch {
247
+ view = null;
248
+ }
249
+ if (view) break;
250
+ }
251
+ cover = toCover(client.config.walrusAggregatorUrl, view);
132
252
  }
133
- return null;
253
+
254
+ let credits: Credit[] | undefined;
255
+ if (wantsCredits) {
256
+ const object = objects[1 + coverFieldIds.length];
257
+ let view: CreditView[] = [];
258
+ if (object && !(object instanceof Error) && object.content) {
259
+ try {
260
+ view = parseReleaseCreditsContent(object.content);
261
+ } catch {
262
+ view = [];
263
+ }
264
+ }
265
+ credits = toCredits(view);
266
+ }
267
+ return {
268
+ release,
269
+ ...(wantsCover ? { cover: cover ?? null } : {}),
270
+ ...(wantsCredits ? { credits: credits ?? [] } : {}),
271
+ };
134
272
  }
135
273
 
136
274
  // ── Release ──────────────────────────────────────────────────────────────────
@@ -144,64 +282,108 @@ export async function readReleaseCover(client: MisoClient, releaseId: string): P
144
282
  * split is inherited from `lib/drops.ts` and is what keeps a half-configured
145
283
  * release renderable.
146
284
  */
147
- export async function getReleaseDetail(client: MisoClient, releaseId: string): Promise<ReleaseDetail> {
148
- const { releaseCredits } = client.config.protocol;
285
+ export type ReleaseInclude = "trackCredits";
149
286
 
150
- const [release, cover, credits] = await Promise.all([
151
- getReleaseById(client.protocol, releaseId),
152
- readReleaseCover(client, releaseId),
153
- getReleaseCredits(client.protocol, releaseId, releaseCredits).catch(() => null),
154
- ]);
287
+ export interface GetReleaseOptions {
288
+ include?: readonly ReleaseInclude[];
289
+ }
290
+
291
+ export async function getReleaseDetail(
292
+ client: MisoClient,
293
+ releaseId: string,
294
+ options: GetReleaseOptions = {},
295
+ ): Promise<ReleaseDetail> {
296
+ const { release, cover, credits } = await getReleaseResources(
297
+ client,
298
+ releaseId,
299
+ ["cover", "credits"],
300
+ );
155
301
 
156
302
  const recordingIds = release.tracks.map((track) => track.recordingId);
157
- const titles = await getRecordingTitles(client.protocol, client.graphql, recordingIds, client.config.protocol.miso);
303
+ const [titles, trackCredits] = await Promise.all([
304
+ getRecordingTitles(
305
+ client.protocol,
306
+ client.graphql,
307
+ recordingIds,
308
+ client.config.protocol.miso,
309
+ ),
310
+ options.include?.includes("trackCredits")
311
+ ? getTrackCreditsForRecordingIds(client, recordingIds)
312
+ : Promise.resolve(undefined),
313
+ ]);
158
314
 
159
- const creditViews = toCredits(credits);
315
+ const creditViews = credits ?? [];
160
316
  return {
161
317
  id: release.id,
162
318
  title: release.title,
163
319
  subtitle: null,
164
320
  state: toWorkState(release.state),
165
- publishedAtMs: release.state.type === "Published" ? release.state.timestampMs : null,
166
- cover,
321
+ publishedAtMs:
322
+ release.state.type === "Published" ? release.state.timestampMs : null,
323
+ cover: cover ?? null,
167
324
  credits: creditViews,
168
325
  primaryArtists: primaryArtistNames(creditViews),
169
326
  discCount: release.tracks.length > 0 ? 1 : 0,
170
327
  tracks: toTracks(release, titles),
328
+ ...(trackCredits !== undefined ? { trackCredits } : {}),
171
329
  };
172
330
  }
173
331
 
332
+ async function getTrackCreditsForRecordingIds(
333
+ client: MisoClient,
334
+ recordingIds: readonly string[],
335
+ ): Promise<Record<string, TrackCredits>> {
336
+ const { compositionCredits, recordingCredits, miso } = client.config.protocol;
337
+ const tracks = await getTrackCreditsByRecordingIds(
338
+ client.protocol,
339
+ client.graphql,
340
+ recordingIds,
341
+ {
342
+ misoPackageId: miso,
343
+ compositionCreditsPackageId: compositionCredits,
344
+ recordingCreditsPackageId: recordingCredits,
345
+ },
346
+ );
347
+ return Object.fromEntries(
348
+ Object.entries(tracks).map(([id, track]) => [
349
+ id,
350
+ {
351
+ compositionCredits: toCredits(track.compositionCredits),
352
+ recordingCredits: {
353
+ credits: toCredits(track.recordingCredits.credits),
354
+ primaryArtistIds: [...track.recordingCredits.primaryArtistIds],
355
+ featuredArtistIds: [...track.recordingCredits.featuredArtistIds],
356
+ },
357
+ } satisfies TrackCredits,
358
+ ]),
359
+ );
360
+ }
361
+
174
362
  /**
175
363
  * Per-track credits for a release, keyed by recording id. A track with no credits
176
364
  * set maps to an empty entry rather than being absent, so a caller can tell
177
365
  * "nothing credited" from "no such track".
178
366
  */
179
- export async function getTrackCredits(client: MisoClient, releaseId: string): Promise<Record<string, TrackCredits>> {
180
- const { recordingCredits } = client.config.protocol;
367
+ export async function getTrackCredits(
368
+ client: MisoClient,
369
+ releaseId: string,
370
+ ): Promise<Record<string, TrackCredits>> {
181
371
  const release = await getReleaseById(client.protocol, releaseId);
182
372
  const recordingIds = release.tracks.map((track) => track.recordingId);
183
-
184
- const entries = await Promise.all(
185
- recordingIds.map(async (id) => {
186
- const view = await getRecordingCredits(client.protocol, id, recordingCredits).catch(() => null);
187
- const credits: TrackCredits = {
188
- credits: toCredits(view?.credits ?? null),
189
- primaryArtistIds: view?.primaryArtistIds ?? [],
190
- featuredArtistIds: view?.featuredArtistIds ?? [],
191
- };
192
- return [id, credits] as const;
193
- }),
194
- );
195
- return Object.fromEntries(entries);
373
+ return getTrackCreditsForRecordingIds(client, recordingIds);
196
374
  }
197
375
 
198
376
  // ── Pressing ─────────────────────────────────────────────────────────────────
199
377
 
200
378
  /** Everything the pressing buy page renders. `null` when no such pressing exists. */
201
- export async function getPressingDetail(client: MisoClient, pressingId: string): Promise<PressingDetail | null> {
379
+ export async function getPressingDetail(
380
+ client: MisoClient,
381
+ pressingId: string,
382
+ options: GetReleaseOptions = {},
383
+ ): Promise<PressingDetail | null> {
202
384
  const pressing = await getDrop(client.protocol, pressingId);
203
385
  if (!pressing) return null;
204
- const release = await getReleaseDetail(client, pressing.releaseId);
386
+ const release = await getReleaseDetail(client, pressing.releaseId, options);
205
387
  return { pressing: toPressingView(pressing), release };
206
388
  }
207
389
 
@@ -210,17 +392,18 @@ export async function getPressingDetail(client: MisoClient, pressingId: string):
210
392
  * object really is a `Drop` before spending reads on it — a release id or a
211
393
  * record id pasted by mistake must come back as `null`, not as a half-built card.
212
394
  */
213
- export async function getDropPreview(client: MisoClient, dropId: string): Promise<DropPreview | null> {
214
- const { object } = await client.protocol.core.getObject({ objectId: dropId, include: { content: true } }).catch(() => ({ object: null }));
215
- if (!object || !/::drop::Drop</.test(object.type ?? "")) return null;
216
-
395
+ export async function getDropPreview(
396
+ client: MisoClient,
397
+ dropId: string,
398
+ ): Promise<DropPreview | null> {
217
399
  const pressing = await getDrop(client.protocol, dropId);
218
400
  if (!pressing?.currencyType) return null;
219
401
 
220
- const [release, cover] = await Promise.all([
221
- getReleaseById(client.protocol, pressing.releaseId),
222
- readReleaseCover(client, pressing.releaseId),
223
- ]);
402
+ const { release, cover } = await getReleaseResources(
403
+ client,
404
+ pressing.releaseId,
405
+ ["cover"],
406
+ );
224
407
 
225
408
  return {
226
409
  pressingId: dropId,
@@ -243,31 +426,51 @@ export async function getDropPreview(client: MisoClient, dropId: string): Promis
243
426
  * Following the release's `CurrentDropKey` pointer keeps this list pointing at the
244
427
  * live edition without anyone touching the config.
245
428
  */
246
- export async function getDiscoverShelf(client: MisoClient): Promise<DiscoverItem[]> {
247
- const items = await Promise.all(client.config.discoverReleaseIds.map((id) => resolveDiscoverItem(client, id)));
248
- return items.filter((d): d is DiscoverItem => d !== null);
249
- }
250
-
251
- async function resolveDiscoverItem(client: MisoClient, releaseId: string): Promise<DiscoverItem | null> {
252
- const drop = await getCurrentDrop(client.protocol, {
253
- releaseId,
254
- misoDropPackageId: client.config.protocol.drop,
255
- });
256
- if (!drop) return null;
257
-
258
- const [release, cover, credits] = await Promise.all([
259
- getReleaseById(client.protocol, releaseId),
260
- readReleaseCover(client, releaseId),
261
- getReleaseCredits(client.protocol, releaseId, client.config.protocol.releaseCredits).catch(() => null),
429
+ export async function getDiscoverShelf(
430
+ client: MisoClient,
431
+ ): Promise<DiscoverItem[]> {
432
+ const configuredIds = [...client.config.discoverReleaseIds];
433
+ const drops = await getCurrentDrops(
434
+ client.protocol,
435
+ configuredIds,
436
+ client.config.protocol.drop,
437
+ );
438
+ const releaseIds = configuredIds.filter((releaseId) => drops[releaseId]);
439
+ if (releaseIds.length === 0) return [];
440
+
441
+ const [releases, coverViews, credits] = await Promise.all([
442
+ getReleasesByIds(client.protocol, releaseIds),
443
+ getReleaseCoversByIds(client.protocol, releaseIds, [
444
+ client.config.protocol.releaseCoverArt,
445
+ ...client.config.protocol.legacyReleaseCoverArt,
446
+ ]).catch(() => ({}) as Partial<Record<string, ReleaseCoverView>>),
447
+ getReleaseCreditsByIds(
448
+ client.protocol,
449
+ releaseIds,
450
+ client.config.protocol.releaseCredits,
451
+ ).catch(() => ({}) as Partial<Record<string, CreditView[]>>),
262
452
  ]);
263
453
 
264
- return {
265
- pressing: toPressingView(drop),
266
- releaseId,
267
- title: release.title,
268
- artist: primaryArtistNames(toCredits(credits)).join(", "),
269
- coverUrl: cover?.still.url ?? null,
270
- };
454
+ return releaseIds.flatMap((releaseId) => {
455
+ const drop = drops[releaseId];
456
+ const release = releases[releaseId];
457
+ if (!drop || !release) return [];
458
+ const cover = toCover(
459
+ client.config.walrusAggregatorUrl,
460
+ coverViews[releaseId] ?? null,
461
+ );
462
+ return [
463
+ {
464
+ pressing: toPressingView(drop),
465
+ releaseId,
466
+ title: release.title,
467
+ artist: primaryArtistNames(toCredits(credits[releaseId] ?? null)).join(
468
+ ", ",
469
+ ),
470
+ coverUrl: cover?.still.url ?? null,
471
+ },
472
+ ];
473
+ });
271
474
  }
272
475
 
273
476
  // ── Record → release ─────────────────────────────────────────────────────────
@@ -280,12 +483,44 @@ async function resolveDiscoverItem(client: MisoClient, releaseId: string): Promi
280
483
  * This answer is IMMUTABLE for the life of the record, which is what lets the
281
484
  * endpoint in front of it cache for a year.
282
485
  */
283
- export async function getRecordAlbum(client: MisoClient, recordId: string): Promise<RecordAlbum | null> {
486
+ export type RecordAlbumInclude = "release" | "trackCredits";
487
+
488
+ export interface GetRecordAlbumOptions {
489
+ include?: readonly RecordAlbumInclude[];
490
+ }
491
+
492
+ export async function getRecordAlbum(
493
+ client: MisoClient,
494
+ recordId: string,
495
+ options: GetRecordAlbumOptions = {},
496
+ ): Promise<RecordAlbum | null> {
284
497
  try {
285
- const { object } = await client.protocol.core.getObject({ objectId: recordId, include: { json: true } });
498
+ const { object } = await client.protocol.core.getObject({
499
+ objectId: recordId,
500
+ include: { json: true },
501
+ });
286
502
  const json = (object?.json ?? {}) as Record<string, unknown>;
287
503
  const raw = json.release_id ?? json.releaseId;
288
- return { recordId, releaseId: typeof raw === "string" && raw ? raw : null };
504
+ const releaseId = typeof raw === "string" && raw ? raw : null;
505
+ const includeRelease =
506
+ options.include?.some(
507
+ (part) => part === "release" || part === "trackCredits",
508
+ ) ?? false;
509
+ const release =
510
+ includeRelease && releaseId
511
+ ? await getReleaseDetail(client, releaseId, {
512
+ include: options.include?.includes("trackCredits")
513
+ ? ["trackCredits"]
514
+ : [],
515
+ })
516
+ : includeRelease
517
+ ? null
518
+ : undefined;
519
+ return {
520
+ recordId,
521
+ releaseId,
522
+ ...(includeRelease ? { release } : {}),
523
+ };
289
524
  } catch (e) {
290
525
  if (isNotFound(e)) return null;
291
526
  throw e;
package/src/read/index.ts CHANGED
@@ -18,7 +18,14 @@ export { createMisoClient } from "./client.ts";
18
18
  export type { MisoClient, CreateMisoClientOptions } from "./client.ts";
19
19
 
20
20
  export { misoConfig, networkFrom } from "./config.ts";
21
- export type { MisoConfig, MisoConfigOverrides, MoneyIds, Network, PartyIds, ProtocolIds } from "./config.ts";
21
+ export type {
22
+ MisoConfig,
23
+ MisoConfigOverrides,
24
+ MoneyIds,
25
+ Network,
26
+ PartyIds,
27
+ ProtocolIds,
28
+ } from "./config.ts";
22
29
 
23
30
  export {
24
31
  currencyInfo,
@@ -27,12 +34,26 @@ export {
27
34
  getPressingDetail,
28
35
  getRecordAlbum,
29
36
  getReleaseDetail,
37
+ getReleaseResources,
30
38
  getTrackCredits,
31
39
  primaryArtistNames,
32
40
  readReleaseCover,
33
41
  } from "./catalog.ts";
42
+ export type {
43
+ GetRecordAlbumOptions,
44
+ GetReleaseOptions,
45
+ RecordAlbumInclude,
46
+ ReleaseInclude,
47
+ ReleaseResourceInclude,
48
+ } from "./catalog.ts";
49
+ export type { ReleaseResources } from "./catalog.ts";
34
50
 
35
- export { getArtistProfile, getFeaturedRelease, getPartySummaries, partyAvatarUrl } from "./artist.ts";
51
+ export {
52
+ getArtistProfile,
53
+ getFeaturedRelease,
54
+ getPartySummaries,
55
+ partyAvatarUrl,
56
+ } from "./artist.ts";
36
57
  export type { ArtistInclude, GetArtistOptions } from "./artist.ts";
37
58
 
38
59
  export { resolveGenreNames } from "./genres.ts";
package/src/read/types.ts CHANGED
@@ -21,7 +21,9 @@
21
21
  // ── Catalog ──────────────────────────────────────────────────────────────────
22
22
 
23
23
  /** Lifecycle state shared by compositions, recordings, and releases. */
24
- export type WorkState = { type: "Initialized" } | { type: "Published"; timestampMs: number };
24
+ export type WorkState =
25
+ | { type: "Initialized" }
26
+ | { type: "Published"; timestampMs: number };
25
27
 
26
28
  /** A cover image's location on Walrus, plus the URL to fetch it from. */
27
29
  export interface CoverImage {
@@ -49,12 +51,18 @@ export interface Credit {
49
51
  * party, not roles the party performed — a producer can be a featured artist. UI
50
52
  * badges the names by intersecting the two.
51
53
  */
52
- export interface TrackCredits {
54
+ export interface RecordingCredits {
53
55
  credits: Credit[];
54
56
  primaryArtistIds: string[];
55
57
  featuredArtistIds: string[];
56
58
  }
57
59
 
60
+ /** Composition writing credits plus recording performance/production credits. */
61
+ export interface TrackCredits {
62
+ compositionCredits: Credit[];
63
+ recordingCredits: RecordingCredits;
64
+ }
65
+
58
66
  /** One track as a tracklist renders it. */
59
67
  export interface TrackView {
60
68
  /** Display number — `"1"`, or `"1.2"` (disc.track) on a multi-disc set. */
@@ -81,6 +89,8 @@ export interface ReleaseDetail {
81
89
  primaryArtists: string[];
82
90
  discCount: number;
83
91
  tracks: TrackView[];
92
+ /** Present only when requested via `include`, keyed by recording id. */
93
+ trackCredits?: Record<string, TrackCredits>;
84
94
  }
85
95
 
86
96
  /** Pricing policy: pay exactly `amount` (fixed), or at least `amount` (floor). */
@@ -137,6 +147,8 @@ export interface RecordAlbum {
137
147
  recordId: string;
138
148
  /** Null when the record object carries no release id. */
139
149
  releaseId: string | null;
150
+ /** Present only when requested via `include`; null when no release is linked. */
151
+ release?: ReleaseDetail | null;
140
152
  }
141
153
 
142
154
  /** The confirmation preview shown when an owner pastes a pressing id to pin. */
@@ -229,6 +241,8 @@ export interface ArtistProfile {
229
241
  /** Present only when requested via `include` — the owner-editor fields. */
230
242
  roles?: string[];
231
243
  tags?: string[];
244
+ /** Present only when requested via `include`; null means nothing is pinned. */
245
+ featured?: FeaturedRelease | null;
232
246
  /** Public avatar URL (miso-api R2 lane; may 404 when unset). */
233
247
  avatarUrl: string;
234
248
  }