@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
package/src/credits.ts CHANGED
@@ -31,10 +31,13 @@
31
31
  import type { ClientWithCoreApi } from "@mysten/sui/client";
32
32
  import { bcs } from "@mysten/sui/bcs";
33
33
  import { deriveDynamicFieldID } from "@mysten/sui/utils";
34
- import type { Transaction, TransactionArgument, TransactionObjectArgument } from "@mysten/sui/transactions";
34
+ import type {
35
+ Transaction,
36
+ TransactionArgument,
37
+ TransactionObjectArgument,
38
+ } from "@mysten/sui/transactions";
35
39
  import type { TxThunk } from "./transactions.ts";
36
40
  import { OPTION_NONE, OPTION_SOME } from "./internal.ts";
37
- import { isNotFound } from "./queries.ts";
38
41
  import * as compositionCredits from "./contracts/composition_credits/composition_credits.ts";
39
42
  import * as compositionPartyRole from "./contracts/composition_credits/composition_party_role.ts";
40
43
  import * as recordingCredits from "./contracts/recording_credits/recording_credits.ts";
@@ -46,7 +49,15 @@ import * as releasePartyRole from "./contracts/release_credits/release_party_rol
46
49
 
47
50
  /** A composition writing role. `Custom` carries a free-form (validated) name. */
48
51
  export type CompositionRole =
49
- | { type: "Adapter" | "Arranger" | "Composer" | "Lyricist" | "Songwriter" | "Translator" }
52
+ | {
53
+ type:
54
+ | "Adapter"
55
+ | "Arranger"
56
+ | "Composer"
57
+ | "Lyricist"
58
+ | "Songwriter"
59
+ | "Translator";
60
+ }
50
61
  | { type: "Custom"; name: string };
51
62
 
52
63
  /** Seniority/prominence level for a recording role. */
@@ -112,9 +123,15 @@ export type ReleaseRole = "Primary" | "Featured";
112
123
  // the constructor move-call; `tx.add` runs it and yields the role value.
113
124
  type RoleThunk = (tx: Transaction) => TransactionObjectArgument;
114
125
  type LevelCtor = (options: { package?: string }) => RoleThunk;
115
- type LeveledRoleCtor = (options: { package?: string; arguments: [TransactionArgument] }) => RoleThunk;
116
-
117
- const COMPOSITION_ROLE_CTOR: Record<Exclude<CompositionRole["type"], "Custom">, LevelCtor> = {
126
+ type LeveledRoleCtor = (options: {
127
+ package?: string;
128
+ arguments: [TransactionArgument];
129
+ }) => RoleThunk;
130
+
131
+ const COMPOSITION_ROLE_CTOR: Record<
132
+ Exclude<CompositionRole["type"], "Custom">,
133
+ LevelCtor
134
+ > = {
118
135
  Adapter: compositionPartyRole.newAdapterRole,
119
136
  Arranger: compositionPartyRole.newArrangerRole,
120
137
  Composer: compositionPartyRole.newComposerRole,
@@ -135,7 +152,10 @@ const RECORDING_LEVEL_CTOR: Record<RecordingRoleLevel, LevelCtor> = {
135
152
  Principal: recordingPartyRole.newPrincipalRoleLevel,
136
153
  };
137
154
 
138
- const RECORDING_LEVELED_ROLE_CTOR: Record<RecordingLeveledRoleType, LeveledRoleCtor> = {
155
+ const RECORDING_LEVELED_ROLE_CTOR: Record<
156
+ RecordingLeveledRoleType,
157
+ LeveledRoleCtor
158
+ > = {
139
159
  Actor: recordingPartyRole.newActorRole,
140
160
  Arranger: recordingPartyRole.newArrangerRole,
141
161
  BandLeader: recordingPartyRole.newBandLeaderRole,
@@ -173,24 +193,44 @@ function levelOption(
173
193
  level: RecordingRoleLevel | undefined,
174
194
  ): TransactionArgument {
175
195
  const levelType = `${pkg}::recording_party_role::RecordingPartyRoleLevel`;
176
- if (!level) return tx.moveCall({ target: OPTION_NONE, typeArguments: [levelType] });
196
+ if (!level)
197
+ return tx.moveCall({ target: OPTION_NONE, typeArguments: [levelType] });
177
198
  const lvl = tx.add(RECORDING_LEVEL_CTOR[level]({ package: pkg }));
178
- return tx.moveCall({ target: OPTION_SOME, typeArguments: [levelType], arguments: [lvl] });
199
+ return tx.moveCall({
200
+ target: OPTION_SOME,
201
+ typeArguments: [levelType],
202
+ arguments: [lvl],
203
+ });
179
204
  }
180
205
 
181
206
  /** Builds one `CompositionPartyRole` value in the PTB. */
182
- function buildCompositionRole(tx: Transaction, pkg: string, role: CompositionRole): TransactionObjectArgument {
207
+ function buildCompositionRole(
208
+ tx: Transaction,
209
+ pkg: string,
210
+ role: CompositionRole,
211
+ ): TransactionObjectArgument {
183
212
  if (role.type === "Custom") {
184
- return tx.add(compositionPartyRole.newCustomRole({ package: pkg, arguments: [role.name] }));
213
+ return tx.add(
214
+ compositionPartyRole.newCustomRole({
215
+ package: pkg,
216
+ arguments: [role.name],
217
+ }),
218
+ );
185
219
  }
186
220
  return tx.add(COMPOSITION_ROLE_CTOR[role.type]({ package: pkg }));
187
221
  }
188
222
 
189
223
  /** Builds one `RecordingPartyRole` value in the PTB. */
190
- function buildRecordingRole(tx: Transaction, pkg: string, role: RecordingRole): TransactionObjectArgument {
224
+ function buildRecordingRole(
225
+ tx: Transaction,
226
+ pkg: string,
227
+ role: RecordingRole,
228
+ ): TransactionObjectArgument {
191
229
  switch (role.type) {
192
230
  case "ArtistsAndRepertoire":
193
- return tx.add(recordingPartyRole.newArtistsAndRepertoireRole({ package: pkg }));
231
+ return tx.add(
232
+ recordingPartyRole.newArtistsAndRepertoireRole({ package: pkg }),
233
+ );
194
234
  case "Copyist":
195
235
  return tx.add(recordingPartyRole.newCopyistRole({ package: pkg }));
196
236
  case "Instrumentalist":
@@ -218,8 +258,15 @@ function buildRecordingRole(tx: Transaction, pkg: string, role: RecordingRole):
218
258
  }
219
259
 
220
260
  /** Builds one `ReleasePartyRole` value in the PTB. */
221
- function buildReleaseRole(tx: Transaction, pkg: string, role: ReleaseRole): TransactionObjectArgument {
222
- const ctor = role === "Primary" ? releasePartyRole.newPrimaryRole : releasePartyRole.newFeaturedRole;
261
+ function buildReleaseRole(
262
+ tx: Transaction,
263
+ pkg: string,
264
+ role: ReleaseRole,
265
+ ): TransactionObjectArgument {
266
+ const ctor =
267
+ role === "Primary"
268
+ ? releasePartyRole.newPrimaryRole
269
+ : releasePartyRole.newFeaturedRole;
223
270
  return tx.add(ctor({ package: pkg }));
224
271
  }
225
272
 
@@ -237,10 +284,13 @@ const MAX_COMPOSITION_ROLES = 5;
237
284
  const MAX_RECORDING_ROLES = 10;
238
285
 
239
286
  function assertDisplayName(fn: string, displayName: string): void {
240
- if (displayName.length === 0) throw new Error(`${fn}: displayName must not be empty`);
287
+ if (displayName.length === 0)
288
+ throw new Error(`${fn}: displayName must not be empty`);
241
289
  const byteLength = new TextEncoder().encode(displayName).length;
242
290
  if (byteLength > MAX_DISPLAY_NAME_BYTES) {
243
- throw new Error(`${fn}: displayName must be at most ${MAX_DISPLAY_NAME_BYTES} bytes of UTF-8 (got ${byteLength})`);
291
+ throw new Error(
292
+ `${fn}: displayName must be at most ${MAX_DISPLAY_NAME_BYTES} bytes of UTF-8 (got ${byteLength})`,
293
+ );
244
294
  }
245
295
  }
246
296
 
@@ -255,9 +305,16 @@ function roleKey(role: CompositionRole | RecordingRole): string {
255
305
  ]);
256
306
  }
257
307
 
258
- function assertRoles(fn: string, roles: (CompositionRole | RecordingRole)[], max: number): void {
308
+ function assertRoles(
309
+ fn: string,
310
+ roles: (CompositionRole | RecordingRole)[],
311
+ max: number,
312
+ ): void {
259
313
  if (roles.length < 1) throw new Error(`${fn}: at least one role is required`);
260
- if (roles.length > max) throw new Error(`${fn}: at most ${max} roles are allowed (got ${roles.length})`);
314
+ if (roles.length > max)
315
+ throw new Error(
316
+ `${fn}: at most ${max} roles are allowed (got ${roles.length})`,
317
+ );
261
318
  const seen = new Set<string>();
262
319
  for (const role of roles) {
263
320
  const key = roleKey(role);
@@ -308,18 +365,33 @@ export interface AttachCompositionCreditParams {
308
365
  * mirroring the Move aborts) when `displayName` is empty or over 200 UTF-8
309
366
  * bytes, or `roles` is empty, has more than 5 entries, or contains duplicates.
310
367
  */
311
- export function attachCompositionCredit(p: AttachCompositionCreditParams): TxThunk {
368
+ export function attachCompositionCredit(
369
+ p: AttachCompositionCreditParams,
370
+ ): TxThunk {
312
371
  assertDisplayName("attachCompositionCredit", p.displayName);
313
372
  assertRoles("attachCompositionCredit", p.roles, MAX_COMPOSITION_ROLES);
314
373
  return (tx) => {
315
374
  const roleType = `${p.compositionCreditsPackageId}::composition_party_role::CompositionPartyRole`;
316
- const roleArgs = p.roles.map((r) => buildCompositionRole(tx, p.compositionCreditsPackageId, r));
317
- const credit = buildCredit(tx, p.misoCreditPackageId, roleType, p.displayName, roleArgs);
375
+ const roleArgs = p.roles.map((r) =>
376
+ buildCompositionRole(tx, p.compositionCreditsPackageId, r),
377
+ );
378
+ const credit = buildCredit(
379
+ tx,
380
+ p.misoCreditPackageId,
381
+ roleType,
382
+ p.displayName,
383
+ roleArgs,
384
+ );
318
385
  tx.add(
319
386
  compositionCredits.addCredit({
320
387
  package: p.compositionCreditsPackageId,
321
388
  typeArguments: [p.compositionShareType],
322
- arguments: [p.compositionId, p.compositionAdminCapId, p.partyId, credit],
389
+ arguments: [
390
+ p.compositionId,
391
+ p.compositionAdminCapId,
392
+ p.partyId,
393
+ credit,
394
+ ],
323
395
  }),
324
396
  );
325
397
  };
@@ -357,8 +429,16 @@ export function attachRecordingCredit(p: AttachRecordingCreditParams): TxThunk {
357
429
  assertRoles("attachRecordingCredit", p.roles, MAX_RECORDING_ROLES);
358
430
  return (tx) => {
359
431
  const roleType = `${p.recordingCreditsPackageId}::recording_party_role::RecordingPartyRole`;
360
- const roleArgs = p.roles.map((r) => buildRecordingRole(tx, p.recordingCreditsPackageId, r));
361
- const credit = buildCredit(tx, p.misoCreditPackageId, roleType, p.displayName, roleArgs);
432
+ const roleArgs = p.roles.map((r) =>
433
+ buildRecordingRole(tx, p.recordingCreditsPackageId, r),
434
+ );
435
+ const credit = buildCredit(
436
+ tx,
437
+ p.misoCreditPackageId,
438
+ roleType,
439
+ p.displayName,
440
+ roleArgs,
441
+ );
362
442
  tx.add(
363
443
  recordingCredits.addCredit({
364
444
  package: p.recordingCreditsPackageId,
@@ -389,7 +469,9 @@ export interface AddRecordingArtistParams {
389
469
  * party must be credited first (via `attachRecordingCredit`) and not already a
390
470
  * primary or featured artist.
391
471
  */
392
- export function addRecordingPrimaryArtist(p: AddRecordingArtistParams): TxThunk {
472
+ export function addRecordingPrimaryArtist(
473
+ p: AddRecordingArtistParams,
474
+ ): TxThunk {
393
475
  return (tx) => {
394
476
  tx.add(
395
477
  recordingCredits.addPrimaryArtist({
@@ -406,7 +488,9 @@ export function addRecordingPrimaryArtist(p: AddRecordingArtistParams): TxThunk
406
488
  * party must be credited first (via `attachRecordingCredit`) and not already a
407
489
  * primary or featured artist.
408
490
  */
409
- export function addRecordingFeaturedArtist(p: AddRecordingArtistParams): TxThunk {
491
+ export function addRecordingFeaturedArtist(
492
+ p: AddRecordingArtistParams,
493
+ ): TxThunk {
410
494
  return (tx) => {
411
495
  tx.add(
412
496
  recordingCredits.addFeaturedArtist({
@@ -445,7 +529,13 @@ export function addReleaseCredit(p: AddReleaseCreditParams): TxThunk {
445
529
  return (tx) => {
446
530
  const roleType = `${p.releaseCreditsPackageId}::release_party_role::ReleasePartyRole`;
447
531
  const roleArg = buildReleaseRole(tx, p.releaseCreditsPackageId, p.role);
448
- const credit = buildCredit(tx, p.misoCreditPackageId, roleType, p.displayName, [roleArg]);
532
+ const credit = buildCredit(
533
+ tx,
534
+ p.misoCreditPackageId,
535
+ roleType,
536
+ p.displayName,
537
+ [roleArg],
538
+ );
449
539
  tx.add(
450
540
  releaseCredits.addCredit({
451
541
  package: p.releaseCreditsPackageId,
@@ -503,25 +593,127 @@ const ReleaseCreditsField = bcs.struct("Field", {
503
593
  value: releaseCredits.ReleaseCredits,
504
594
  });
505
595
 
506
- const COMPOSITION_CREDITS_KEY_BYTES = compositionCredits.ExtensionKey.serialize([false]).toBytes();
507
- const RECORDING_CREDITS_KEY_BYTES = recordingCredits.ExtensionKey.serialize([false]).toBytes();
508
- const RELEASE_CREDITS_KEY_BYTES = releaseCredits.ExtensionKey.serialize([false]).toBytes();
596
+ const COMPOSITION_CREDITS_KEY_BYTES = compositionCredits.ExtensionKey.serialize(
597
+ [false],
598
+ ).toBytes();
599
+ const RECORDING_CREDITS_KEY_BYTES = recordingCredits.ExtensionKey.serialize([
600
+ false,
601
+ ]).toBytes();
602
+ const RELEASE_CREDITS_KEY_BYTES = releaseCredits.ExtensionKey.serialize([
603
+ false,
604
+ ]).toBytes();
605
+
606
+ type CreditFieldKind =
607
+ | "composition"
608
+ | "compositionLegacy"
609
+ | "recording"
610
+ | "recordingLegacy"
611
+ | "release";
612
+
613
+ interface CreditFieldTarget {
614
+ workId: string;
615
+ kind: CreditFieldKind;
616
+ fieldId: string;
617
+ }
509
618
 
510
- /** Fetches the BCS content of a work's credits dynamic field, or `null` if none. */
511
- async function fetchCreditsField(
619
+ /**
620
+ * Fetch many derived credit fields through one Core bulk request.
621
+ *
622
+ * Composition and recording extensions have one legacy key spelling. Both IDs
623
+ * ride in the same request, so fallback compatibility does not add a sequential
624
+ * round trip when the current key is absent.
625
+ */
626
+ async function fetchCreditFields(
512
627
  client: ClientWithCoreApi,
513
- workId: string,
514
- keyType: string,
515
- keyBytes: Uint8Array,
516
- ): Promise<Uint8Array | null> {
517
- const fieldId = deriveDynamicFieldID(workId, keyType, keyBytes);
518
- try {
519
- const { object } = await client.core.getObject({ objectId: fieldId, include: { content: true } });
520
- return object?.content ?? null;
521
- } catch (e) {
522
- if (isNotFound(e)) return null;
523
- throw e;
524
- }
628
+ targets: readonly CreditFieldTarget[],
629
+ ): Promise<Map<string, Uint8Array>> {
630
+ if (targets.length === 0) return new Map();
631
+ const { objects } = await client.core.getObjects({
632
+ objectIds: targets.map((target) => target.fieldId),
633
+ include: { content: true },
634
+ });
635
+ const contents = new Map<string, Uint8Array>();
636
+ objects.forEach((object, index) => {
637
+ if (object instanceof Error || !object.content) return;
638
+ const target = targets[index];
639
+ if (target) contents.set(target.fieldId, object.content);
640
+ });
641
+ return contents;
642
+ }
643
+
644
+ function compositionCreditTargets(
645
+ compositionIds: readonly string[],
646
+ packageId: string,
647
+ ): CreditFieldTarget[] {
648
+ return compositionIds.flatMap((workId) => [
649
+ {
650
+ workId,
651
+ kind: "composition" as const,
652
+ fieldId: deriveDynamicFieldID(
653
+ workId,
654
+ `${packageId}::composition_credits::ExtensionKey`,
655
+ COMPOSITION_CREDITS_KEY_BYTES,
656
+ ),
657
+ },
658
+ {
659
+ workId,
660
+ kind: "compositionLegacy" as const,
661
+ fieldId: deriveDynamicFieldID(
662
+ workId,
663
+ `${packageId}::composition_credits::CompositionCreditsKey`,
664
+ COMPOSITION_CREDITS_KEY_BYTES,
665
+ ),
666
+ },
667
+ ]);
668
+ }
669
+
670
+ function recordingCreditTargets(
671
+ recordingIds: readonly string[],
672
+ packageId: string,
673
+ ): CreditFieldTarget[] {
674
+ return recordingIds.flatMap((workId) => [
675
+ {
676
+ workId,
677
+ kind: "recording" as const,
678
+ fieldId: deriveDynamicFieldID(
679
+ workId,
680
+ `${packageId}::recording_credits::ExtensionKey`,
681
+ RECORDING_CREDITS_KEY_BYTES,
682
+ ),
683
+ },
684
+ {
685
+ workId,
686
+ kind: "recordingLegacy" as const,
687
+ fieldId: deriveDynamicFieldID(
688
+ workId,
689
+ `${packageId}::recording_credits::RecordingCreditsKey`,
690
+ RECORDING_CREDITS_KEY_BYTES,
691
+ ),
692
+ },
693
+ ]);
694
+ }
695
+
696
+ function releaseCreditTargets(
697
+ releaseIds: readonly string[],
698
+ packageId: string,
699
+ ): CreditFieldTarget[] {
700
+ return releaseIds.map((workId) => ({
701
+ workId,
702
+ kind: "release",
703
+ fieldId: releaseCreditsFieldId(workId, packageId),
704
+ }));
705
+ }
706
+
707
+ /** Deterministic dynamic-field id for a release's billing credits. */
708
+ export function releaseCreditsFieldId(
709
+ releaseId: string,
710
+ releaseCreditsPackageId: string,
711
+ ): string {
712
+ return deriveDynamicFieldID(
713
+ releaseId,
714
+ `${releaseCreditsPackageId}::release_credits::ExtensionKey`,
715
+ RELEASE_CREDITS_KEY_BYTES,
716
+ );
525
717
  }
526
718
 
527
719
  // The credits container shape, checked structurally against the parse output of
@@ -530,7 +722,9 @@ async function fetchCreditsField(
530
722
  // field rename (`contents`/`key`/`value`/`display_name`/`roles`, or the
531
723
  // `credits`/`primary_artist_ids`/`featured_artist_ids` fields at the call
532
724
  // sites) fails typecheck here instead of silently misparsing.
533
- type ParsedCreditsMap<Role> = { contents: { key: string; value: { display_name: string; roles: Role[] } }[] };
725
+ type ParsedCreditsMap<Role> = {
726
+ contents: { key: string; value: { display_name: string; roles: Role[] } }[];
727
+ };
534
728
 
535
729
  function creditViews<Role extends ParsedEnum>(
536
730
  credits: ParsedCreditsMap<Role>,
@@ -553,6 +747,14 @@ function releaseRoleLabel(role: ParsedEnum): string {
553
747
  return role.$kind;
554
748
  }
555
749
 
750
+ /** Parse a fetched release-credit dynamic field. */
751
+ export function parseReleaseCreditsContent(content: Uint8Array): CreditView[] {
752
+ return creditViews(
753
+ ReleaseCreditsField.parse(content).value.credits,
754
+ releaseRoleLabel,
755
+ );
756
+ }
757
+
556
758
  // `name()` semantics plus the recording extras: the instrument (for
557
759
  // `Instrumentalist`) and the optional level, both surfaced into the label.
558
760
  function recordingRoleLabel(role: ParsedEnum): string {
@@ -581,7 +783,9 @@ function recordingRoleLabel(role: ParsedEnum): string {
581
783
  base = role.$kind;
582
784
  const payload = role[role.$kind];
583
785
  level =
584
- payload && typeof payload === "object" && "$kind" in payload ? (payload as ParsedEnum) : null;
786
+ payload && typeof payload === "object" && "$kind" in payload
787
+ ? (payload as ParsedEnum)
788
+ : null;
585
789
  break;
586
790
  }
587
791
  }
@@ -597,21 +801,45 @@ export async function getCompositionCredits(
597
801
  compositionId: string,
598
802
  compositionCreditsPackageId: string,
599
803
  ): Promise<CreditView[] | null> {
600
- const content =
601
- (await fetchCreditsField(
602
- client,
603
- compositionId,
604
- `${compositionCreditsPackageId}::composition_credits::ExtensionKey`,
605
- COMPOSITION_CREDITS_KEY_BYTES,
606
- )) ??
607
- (await fetchCreditsField(
608
- client,
609
- compositionId,
610
- `${compositionCreditsPackageId}::composition_credits::CompositionCreditsKey`,
611
- COMPOSITION_CREDITS_KEY_BYTES,
612
- ));
613
- if (!content) return null;
614
- return creditViews(CompositionCreditsField.parse(content).value.credits, compositionRoleLabel);
804
+ return (
805
+ (
806
+ await getCompositionCreditsByIds(
807
+ client,
808
+ [compositionId],
809
+ compositionCreditsPackageId,
810
+ )
811
+ )[compositionId] ?? null
812
+ );
813
+ }
814
+
815
+ /** Composition credits for many works in one Core bulk request. */
816
+ export async function getCompositionCreditsByIds(
817
+ client: ClientWithCoreApi,
818
+ compositionIdsInput: readonly string[],
819
+ compositionCreditsPackageId: string,
820
+ ): Promise<Partial<Record<string, CreditView[]>>> {
821
+ const compositionIds = [...new Set(compositionIdsInput)];
822
+ const targets = compositionCreditTargets(
823
+ compositionIds,
824
+ compositionCreditsPackageId,
825
+ );
826
+ const contents = await fetchCreditFields(client, targets);
827
+ const out: Partial<Record<string, CreditView[]>> = {};
828
+ for (const compositionId of compositionIds) {
829
+ const candidates = targets.filter(
830
+ (target) => target.workId === compositionId,
831
+ );
832
+ const content = candidates
833
+ .map((target) => contents.get(target.fieldId))
834
+ .find((value): value is Uint8Array => value !== undefined);
835
+ if (content) {
836
+ out[compositionId] = creditViews(
837
+ CompositionCreditsField.parse(content).value.credits,
838
+ compositionRoleLabel,
839
+ );
840
+ }
841
+ }
842
+ return out;
615
843
  }
616
844
 
617
845
  /**
@@ -623,26 +851,46 @@ export async function getRecordingCredits(
623
851
  recordingId: string,
624
852
  recordingCreditsPackageId: string,
625
853
  ): Promise<RecordingCreditsView | null> {
626
- const content =
627
- (await fetchCreditsField(
628
- client,
629
- recordingId,
630
- `${recordingCreditsPackageId}::recording_credits::ExtensionKey`,
631
- RECORDING_CREDITS_KEY_BYTES,
632
- )) ??
633
- (await fetchCreditsField(
634
- client,
635
- recordingId,
636
- `${recordingCreditsPackageId}::recording_credits::RecordingCreditsKey`,
637
- RECORDING_CREDITS_KEY_BYTES,
638
- ));
639
- if (!content) return null;
640
- const value = RecordingCreditsField.parse(content).value;
641
- return {
642
- credits: creditViews(value.credits, recordingRoleLabel),
643
- primaryArtistIds: value.primary_artist_ids.contents,
644
- featuredArtistIds: value.featured_artist_ids.contents,
645
- };
854
+ return (
855
+ (
856
+ await getRecordingCreditsByIds(
857
+ client,
858
+ [recordingId],
859
+ recordingCreditsPackageId,
860
+ )
861
+ )[recordingId] ?? null
862
+ );
863
+ }
864
+
865
+ /** Recording credits for many works in one Core bulk request. */
866
+ export async function getRecordingCreditsByIds(
867
+ client: ClientWithCoreApi,
868
+ recordingIdsInput: readonly string[],
869
+ recordingCreditsPackageId: string,
870
+ ): Promise<Partial<Record<string, RecordingCreditsView>>> {
871
+ const recordingIds = [...new Set(recordingIdsInput)];
872
+ const targets = recordingCreditTargets(
873
+ recordingIds,
874
+ recordingCreditsPackageId,
875
+ );
876
+ const contents = await fetchCreditFields(client, targets);
877
+ const out: Partial<Record<string, RecordingCreditsView>> = {};
878
+ for (const recordingId of recordingIds) {
879
+ const candidates = targets.filter(
880
+ (target) => target.workId === recordingId,
881
+ );
882
+ const content = candidates
883
+ .map((target) => contents.get(target.fieldId))
884
+ .find((value): value is Uint8Array => value !== undefined);
885
+ if (!content) continue;
886
+ const value = RecordingCreditsField.parse(content).value;
887
+ out[recordingId] = {
888
+ credits: creditViews(value.credits, recordingRoleLabel),
889
+ primaryArtistIds: value.primary_artist_ids.contents,
890
+ featuredArtistIds: value.featured_artist_ids.contents,
891
+ };
892
+ }
893
+ return out;
646
894
  }
647
895
 
648
896
  /**
@@ -654,12 +902,28 @@ export async function getReleaseCredits(
654
902
  releaseId: string,
655
903
  releaseCreditsPackageId: string,
656
904
  ): Promise<CreditView[] | null> {
657
- const content = await fetchCreditsField(
658
- client,
659
- releaseId,
660
- `${releaseCreditsPackageId}::release_credits::ExtensionKey`,
661
- RELEASE_CREDITS_KEY_BYTES,
905
+ return (
906
+ (
907
+ await getReleaseCreditsByIds(client, [releaseId], releaseCreditsPackageId)
908
+ )[releaseId] ?? null
662
909
  );
663
- if (!content) return null;
664
- return creditViews(ReleaseCreditsField.parse(content).value.credits, releaseRoleLabel);
910
+ }
911
+
912
+ /** Release billing credits for many releases in one Core bulk request. */
913
+ export async function getReleaseCreditsByIds(
914
+ client: ClientWithCoreApi,
915
+ releaseIdsInput: readonly string[],
916
+ releaseCreditsPackageId: string,
917
+ ): Promise<Partial<Record<string, CreditView[]>>> {
918
+ const releaseIds = [...new Set(releaseIdsInput)];
919
+ const targets = releaseCreditTargets(releaseIds, releaseCreditsPackageId);
920
+ const contents = await fetchCreditFields(client, targets);
921
+ const out: Partial<Record<string, CreditView[]>> = {};
922
+ for (const target of targets) {
923
+ const content = contents.get(target.fieldId);
924
+ if (content) {
925
+ out[target.workId] = parseReleaseCreditsContent(content);
926
+ }
927
+ }
928
+ return out;
665
929
  }