@misofm/platform 0.0.0 → 0.18.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.
- package/LICENSE +201 -0
- package/README.md +686 -3
- package/dist/auth.d.ts +73 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +193 -0
- package/dist/auth.js.map +1 -0
- package/dist/catalog.d.ts +48 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +92 -0
- package/dist/catalog.js.map +1 -0
- package/dist/client.d.ts +462 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +788 -0
- package/dist/client.js.map +1 -0
- package/dist/contracts.d.ts +34 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +49 -0
- package/dist/contracts.js.map +1 -0
- package/dist/cover.d.ts +69 -0
- package/dist/cover.d.ts.map +1 -0
- package/dist/cover.js +132 -0
- package/dist/cover.js.map +1 -0
- package/dist/credits.d.ts +200 -0
- package/dist/credits.d.ts.map +1 -0
- package/dist/credits.js +465 -0
- package/dist/credits.js.map +1 -0
- package/dist/deployments.d.ts +224 -0
- package/dist/deployments.d.ts.map +1 -0
- package/dist/deployments.js +177 -0
- package/dist/deployments.js.map +1 -0
- package/dist/execute.d.ts +30 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +31 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +13 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +41 -0
- package/dist/internal.js.map +1 -0
- package/dist/mix.d.ts +144 -0
- package/dist/mix.d.ts.map +1 -0
- package/dist/mix.js +656 -0
- package/dist/mix.js.map +1 -0
- package/dist/pressing.d.ts +137 -0
- package/dist/pressing.d.ts.map +1 -0
- package/dist/pressing.js +390 -0
- package/dist/pressing.js.map +1 -0
- package/dist/publication.d.ts +216 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +872 -0
- package/dist/publication.js.map +1 -0
- package/dist/queries.d.ts +2 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +8 -0
- package/dist/queries.js.map +1 -0
- package/dist/read/artist.d.ts +18 -0
- package/dist/read/artist.d.ts.map +1 -0
- package/dist/read/artist.js +86 -0
- package/dist/read/artist.js.map +1 -0
- package/dist/read/catalog.d.ts +93 -0
- package/dist/read/catalog.d.ts.map +1 -0
- package/dist/read/catalog.js +441 -0
- package/dist/read/catalog.js.map +1 -0
- package/dist/read/client.d.ts +37 -0
- package/dist/read/client.d.ts.map +1 -0
- package/dist/read/client.js +50 -0
- package/dist/read/client.js.map +1 -0
- package/dist/read/config.d.ts +68 -0
- package/dist/read/config.d.ts.map +1 -0
- package/dist/read/config.js +63 -0
- package/dist/read/config.js.map +1 -0
- package/dist/read/genres.d.ts +7 -0
- package/dist/read/genres.d.ts.map +1 -0
- package/dist/read/genres.js +38 -0
- package/dist/read/genres.js.map +1 -0
- package/dist/read/index.d.ts +14 -0
- package/dist/read/index.d.ts.map +1 -0
- package/dist/read/index.js +23 -0
- package/dist/read/index.js.map +1 -0
- package/dist/read/internal/scalars.d.ts +23 -0
- package/dist/read/internal/scalars.d.ts.map +1 -0
- package/dist/read/internal/scalars.js +57 -0
- package/dist/read/internal/scalars.js.map +1 -0
- package/dist/read/internal/walrus.d.ts +6 -0
- package/dist/read/internal/walrus.d.ts.map +1 -0
- package/dist/read/internal/walrus.js +29 -0
- package/dist/read/internal/walrus.js.map +1 -0
- package/dist/read/receipts.d.ts +41 -0
- package/dist/read/receipts.d.ts.map +1 -0
- package/dist/read/receipts.js +398 -0
- package/dist/read/receipts.js.map +1 -0
- package/dist/read/types.d.ts +328 -0
- package/dist/read/types.d.ts.map +1 -0
- package/dist/read/types.js +21 -0
- package/dist/read/types.js.map +1 -0
- package/dist/read/wallet.d.ts +74 -0
- package/dist/read/wallet.d.ts.map +1 -0
- package/dist/read/wallet.js +578 -0
- package/dist/read/wallet.js.map +1 -0
- package/dist/read/works.d.ts +29 -0
- package/dist/read/works.d.ts.map +1 -0
- package/dist/read/works.js +191 -0
- package/dist/read/works.js.map +1 -0
- package/dist/recording-extensions.d.ts +62 -0
- package/dist/recording-extensions.d.ts.map +1 -0
- package/dist/recording-extensions.js +159 -0
- package/dist/recording-extensions.js.map +1 -0
- package/dist/release-extensions.d.ts +93 -0
- package/dist/release-extensions.d.ts.map +1 -0
- package/dist/release-extensions.js +190 -0
- package/dist/release-extensions.js.map +1 -0
- package/dist/release-graph.d.ts +65 -0
- package/dist/release-graph.d.ts.map +1 -0
- package/dist/release-graph.js +136 -0
- package/dist/release-graph.js.map +1 -0
- package/dist/share-template.d.ts +3 -0
- package/dist/share-template.d.ts.map +1 -0
- package/dist/share-template.js +46 -0
- package/dist/share-template.js.map +1 -0
- package/dist/share.d.ts +67 -0
- package/dist/share.d.ts.map +1 -0
- package/dist/share.js +157 -0
- package/dist/share.js.map +1 -0
- package/dist/transactions.d.ts +216 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/transactions.js +314 -0
- package/dist/transactions.js.map +1 -0
- package/dist/vault.d.ts +364 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +507 -0
- package/dist/vault.js.map +1 -0
- package/package.json +145 -5
- package/src/auth.ts +275 -0
- package/src/catalog.ts +189 -0
- package/src/client.ts +1320 -0
- package/src/contracts.ts +54 -0
- package/src/cover.ts +240 -0
- package/src/credits.ts +906 -0
- package/src/deployments.ts +357 -0
- package/src/execute.ts +52 -0
- package/src/index.ts +69 -0
- package/src/internal.ts +46 -0
- package/src/mix.ts +910 -0
- package/src/pressing.ts +705 -0
- package/src/publication.ts +1214 -0
- package/src/queries.ts +9 -0
- package/src/read/artist.ts +118 -0
- package/src/read/catalog.ts +668 -0
- package/src/read/client.ts +87 -0
- package/src/read/config.ts +138 -0
- package/src/read/genres.ts +37 -0
- package/src/read/index.ts +85 -0
- package/src/read/internal/scalars.ts +59 -0
- package/src/read/internal/walrus.ts +42 -0
- package/src/read/receipts.ts +476 -0
- package/src/read/types.ts +415 -0
- package/src/read/wallet.ts +687 -0
- package/src/read/works.ts +297 -0
- package/src/recording-extensions.ts +256 -0
- package/src/release-extensions.ts +311 -0
- package/src/release-graph.ts +219 -0
- package/src/share-template.ts +55 -0
- package/src/share.ts +261 -0
- package/src/transactions.ts +542 -0
- package/src/vault.ts +1009 -0
package/src/credits.ts
ADDED
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Contributor credits. A credit pairs a party with a display name and one or more
|
|
5
|
+
// domain-specific roles (`miso_credit::credit::Credit<Role>`), attached to a work
|
|
6
|
+
// via a dynamic field on the work's UID and gated by the work's admin cap. Three
|
|
7
|
+
// first-party extensions carry the domain role vocabularies:
|
|
8
|
+
//
|
|
9
|
+
// - composition_credits — writing credits (Composer, Lyricist, …), no level.
|
|
10
|
+
// - recording_credits — production/performance credits (Producer, Vocalist,
|
|
11
|
+
// Instrumentalist, …), each with an optional seniority
|
|
12
|
+
// level; plus primary/featured artist designation.
|
|
13
|
+
// - release_credits — top-line billing, exactly one role (Primary|Featured).
|
|
14
|
+
//
|
|
15
|
+
// A credit is built in-PTB from raw role constructor move-calls (the role enums
|
|
16
|
+
// are closed — only the extension's `new_*_role` functions can mint them), wrapped
|
|
17
|
+
// in `credit::new(display_name, roles)`, then attached with the extension's
|
|
18
|
+
// `add_credit` (which borrows the work `&mut` via its cap-gated `uid_mut`).
|
|
19
|
+
//
|
|
20
|
+
// Writers mirror `cover.ts`: they return a `TxThunk` and take explicit on-chain
|
|
21
|
+
// package ids. Generic works (Composition, Recording) additionally require the
|
|
22
|
+
// share coin type argument(s) so the `&mut Work<Share>` / `&AdminCap<Share>` calls
|
|
23
|
+
// resolve.
|
|
24
|
+
//
|
|
25
|
+
// The role vocabularies below are the clearest case for why extensions ship from
|
|
26
|
+
// the platform package: which roles exist, what they are called, and how many a
|
|
27
|
+
// work may carry are editorial decisions Miso made. The protocol has no opinion
|
|
28
|
+
// about any of it — it only provides the cap-gated `uid_mut` hook these attach
|
|
29
|
+
// through.
|
|
30
|
+
|
|
31
|
+
import type { ClientWithCoreApi } from "@mysten/sui/client";
|
|
32
|
+
import { bcs } from "@mysten/sui/bcs";
|
|
33
|
+
import { deriveDynamicFieldID } from "@mysten/sui/utils";
|
|
34
|
+
import type {
|
|
35
|
+
Transaction,
|
|
36
|
+
TransactionArgument,
|
|
37
|
+
TransactionObjectArgument,
|
|
38
|
+
} from "@mysten/sui/transactions";
|
|
39
|
+
import type { TxThunk } from "./transactions.ts";
|
|
40
|
+
import { OPTION_NONE, OPTION_SOME } from "./internal.ts";
|
|
41
|
+
import * as compositionCredits from "@misofm/protocol/contracts/composition_credits/composition_credits";
|
|
42
|
+
import * as compositionPartyRole from "@misofm/protocol/contracts/composition_credits/composition_party_role";
|
|
43
|
+
import * as recordingCredits from "@misofm/protocol/contracts/recording_credits/recording_credits";
|
|
44
|
+
import * as recordingPartyRole from "@misofm/protocol/contracts/recording_credits/recording_party_role";
|
|
45
|
+
import * as releaseCredits from "@misofm/protocol/contracts/release_credits/release_credits";
|
|
46
|
+
import * as releasePartyRole from "@misofm/protocol/contracts/release_credits/release_party_role";
|
|
47
|
+
import { directAdminCap, invokeWithAdminCap, type AdminCapAuthority, type ObjectInput } from "./vault.ts";
|
|
48
|
+
|
|
49
|
+
function object(tx: Transaction, value: ObjectInput): TransactionObjectArgument {
|
|
50
|
+
return typeof value === "string" ? tx.object(value) : value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type CompositionAuthorityInput =
|
|
54
|
+
| { readonly authority: AdminCapAuthority; readonly compositionAdminCapId?: never }
|
|
55
|
+
| { readonly authority?: never; readonly compositionAdminCapId: string };
|
|
56
|
+
type RecordingAuthorityInput =
|
|
57
|
+
| { readonly authority: AdminCapAuthority; readonly recordingAdminCapId?: never }
|
|
58
|
+
| { readonly authority?: never; readonly recordingAdminCapId: string };
|
|
59
|
+
type ReleaseAuthorityInput =
|
|
60
|
+
| { readonly authority: AdminCapAuthority; readonly releaseAdminCapId?: never }
|
|
61
|
+
| { readonly authority?: never; readonly releaseAdminCapId: string };
|
|
62
|
+
|
|
63
|
+
function compositionAuthorityOf(input: CompositionAuthorityInput): AdminCapAuthority {
|
|
64
|
+
if (input.authority !== undefined) return input.authority;
|
|
65
|
+
return directAdminCap(input.compositionAdminCapId);
|
|
66
|
+
}
|
|
67
|
+
function recordingAuthorityOf(input: RecordingAuthorityInput): AdminCapAuthority {
|
|
68
|
+
if (input.authority !== undefined) return input.authority;
|
|
69
|
+
return directAdminCap(input.recordingAdminCapId);
|
|
70
|
+
}
|
|
71
|
+
function releaseAuthorityOf(input: ReleaseAuthorityInput): AdminCapAuthority {
|
|
72
|
+
if (input.authority !== undefined) return input.authority;
|
|
73
|
+
return directAdminCap(input.releaseAdminCapId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Role model ────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/** A composition writing role. `Custom` carries a free-form (validated) name. */
|
|
79
|
+
export type CompositionRole =
|
|
80
|
+
| {
|
|
81
|
+
type:
|
|
82
|
+
| "Adapter"
|
|
83
|
+
| "Arranger"
|
|
84
|
+
| "Composer"
|
|
85
|
+
| "Lyricist"
|
|
86
|
+
| "Songwriter"
|
|
87
|
+
| "Translator";
|
|
88
|
+
}
|
|
89
|
+
| { type: "Custom"; name: string };
|
|
90
|
+
|
|
91
|
+
/** Seniority/prominence level for a recording role. */
|
|
92
|
+
export type RecordingRoleLevel =
|
|
93
|
+
| "Additional"
|
|
94
|
+
| "Assistant"
|
|
95
|
+
| "Associate"
|
|
96
|
+
| "Backing"
|
|
97
|
+
| "Executive"
|
|
98
|
+
| "Featured"
|
|
99
|
+
| "Lead"
|
|
100
|
+
| "Primary"
|
|
101
|
+
| "Principal";
|
|
102
|
+
|
|
103
|
+
/** Recording role base names that carry an optional level. */
|
|
104
|
+
export type RecordingLeveledRoleType =
|
|
105
|
+
| "Actor"
|
|
106
|
+
| "Arranger"
|
|
107
|
+
| "BandLeader"
|
|
108
|
+
| "Choir"
|
|
109
|
+
| "ChoirMaster"
|
|
110
|
+
| "ConcertMaster"
|
|
111
|
+
| "Conductor"
|
|
112
|
+
| "Contractor"
|
|
113
|
+
| "DJ"
|
|
114
|
+
| "Editor"
|
|
115
|
+
| "Engineer"
|
|
116
|
+
| "Ensemble"
|
|
117
|
+
| "MasteringEngineer"
|
|
118
|
+
| "MixingEngineer"
|
|
119
|
+
| "MusicDirector"
|
|
120
|
+
| "MusicSupervisor"
|
|
121
|
+
| "Narrator"
|
|
122
|
+
| "Orchestra"
|
|
123
|
+
| "Orchestrator"
|
|
124
|
+
| "Performer"
|
|
125
|
+
| "Producer"
|
|
126
|
+
| "Programmer"
|
|
127
|
+
| "RecordingEngineer"
|
|
128
|
+
| "RemixingEngineer"
|
|
129
|
+
| "Soloist"
|
|
130
|
+
| "SoundDesigner"
|
|
131
|
+
| "Speaker"
|
|
132
|
+
| "Vocalist";
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A recording production/performance role. Most roles take an optional `level`;
|
|
136
|
+
* `Instrumentalist` also carries the instrument name; `Custom` carries a free-form
|
|
137
|
+
* (validated) name; `ArtistsAndRepertoire` and `Copyist` are clerical and take no
|
|
138
|
+
* level.
|
|
139
|
+
*/
|
|
140
|
+
export type RecordingRole =
|
|
141
|
+
| { type: RecordingLeveledRoleType; level?: RecordingRoleLevel }
|
|
142
|
+
| { type: "Instrumentalist"; instrument: string; level?: RecordingRoleLevel }
|
|
143
|
+
| { type: "Custom"; name: string; level?: RecordingRoleLevel }
|
|
144
|
+
| { type: "ArtistsAndRepertoire" }
|
|
145
|
+
| { type: "Copyist" };
|
|
146
|
+
|
|
147
|
+
/** A release billing role — a release credit carries exactly one of these. */
|
|
148
|
+
export type ReleaseRole = "Primary" | "Featured";
|
|
149
|
+
|
|
150
|
+
// Role constructor lookups. Each generated `new_*_role` returns a thunk that adds
|
|
151
|
+
// the constructor move-call; `tx.add` runs it and yields the role value.
|
|
152
|
+
type RoleThunk = (tx: Transaction) => TransactionObjectArgument;
|
|
153
|
+
type LevelCtor = (options: { package?: string }) => RoleThunk;
|
|
154
|
+
type LeveledRoleCtor = (options: {
|
|
155
|
+
package?: string;
|
|
156
|
+
arguments: [TransactionArgument];
|
|
157
|
+
}) => RoleThunk;
|
|
158
|
+
|
|
159
|
+
const COMPOSITION_ROLE_CTOR: Record<
|
|
160
|
+
Exclude<CompositionRole["type"], "Custom">,
|
|
161
|
+
LevelCtor
|
|
162
|
+
> = {
|
|
163
|
+
Adapter: compositionPartyRole.newAdapterRole,
|
|
164
|
+
Arranger: compositionPartyRole.newArrangerRole,
|
|
165
|
+
Composer: compositionPartyRole.newComposerRole,
|
|
166
|
+
Lyricist: compositionPartyRole.newLyricistRole,
|
|
167
|
+
Songwriter: compositionPartyRole.newSongwriterRole,
|
|
168
|
+
Translator: compositionPartyRole.newTranslatorRole,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const RECORDING_LEVEL_CTOR: Record<RecordingRoleLevel, LevelCtor> = {
|
|
172
|
+
Additional: recordingPartyRole.newAdditionalRoleLevel,
|
|
173
|
+
Assistant: recordingPartyRole.newAssistantRoleLevel,
|
|
174
|
+
Associate: recordingPartyRole.newAssociateRoleLevel,
|
|
175
|
+
Backing: recordingPartyRole.newBackingRoleLevel,
|
|
176
|
+
Executive: recordingPartyRole.newExecutiveRoleLevel,
|
|
177
|
+
Featured: recordingPartyRole.newFeaturedRoleLevel,
|
|
178
|
+
Lead: recordingPartyRole.newLeadRoleLevel,
|
|
179
|
+
Primary: recordingPartyRole.newPrimaryRoleLevel,
|
|
180
|
+
Principal: recordingPartyRole.newPrincipalRoleLevel,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const RECORDING_LEVELED_ROLE_CTOR: Record<
|
|
184
|
+
RecordingLeveledRoleType,
|
|
185
|
+
LeveledRoleCtor
|
|
186
|
+
> = {
|
|
187
|
+
Actor: recordingPartyRole.newActorRole,
|
|
188
|
+
Arranger: recordingPartyRole.newArrangerRole,
|
|
189
|
+
BandLeader: recordingPartyRole.newBandLeaderRole,
|
|
190
|
+
Choir: recordingPartyRole.newChoirRole,
|
|
191
|
+
ChoirMaster: recordingPartyRole.newChoirMasterRole,
|
|
192
|
+
ConcertMaster: recordingPartyRole.newConcertMasterRole,
|
|
193
|
+
Conductor: recordingPartyRole.newConductorRole,
|
|
194
|
+
Contractor: recordingPartyRole.newContractorRole,
|
|
195
|
+
DJ: recordingPartyRole.newDjRole,
|
|
196
|
+
Editor: recordingPartyRole.newEditorRole,
|
|
197
|
+
Engineer: recordingPartyRole.newEngineerRole,
|
|
198
|
+
Ensemble: recordingPartyRole.newEnsembleRole,
|
|
199
|
+
MasteringEngineer: recordingPartyRole.newMasteringEngineerRole,
|
|
200
|
+
MixingEngineer: recordingPartyRole.newMixingEngineerRole,
|
|
201
|
+
MusicDirector: recordingPartyRole.newMusicDirectorRole,
|
|
202
|
+
MusicSupervisor: recordingPartyRole.newMusicSupervisorRole,
|
|
203
|
+
Narrator: recordingPartyRole.newNarratorRole,
|
|
204
|
+
Orchestra: recordingPartyRole.newOrchestraRole,
|
|
205
|
+
Orchestrator: recordingPartyRole.newOrchestratorRole,
|
|
206
|
+
Performer: recordingPartyRole.newPerformerRole,
|
|
207
|
+
Producer: recordingPartyRole.newProducerRole,
|
|
208
|
+
Programmer: recordingPartyRole.newProgrammerRole,
|
|
209
|
+
RecordingEngineer: recordingPartyRole.newRecordingEngineerRole,
|
|
210
|
+
RemixingEngineer: recordingPartyRole.newRemixingEngineerRole,
|
|
211
|
+
Soloist: recordingPartyRole.newSoloistRole,
|
|
212
|
+
SoundDesigner: recordingPartyRole.newSoundDesignerRole,
|
|
213
|
+
Speaker: recordingPartyRole.newSpeakerRole,
|
|
214
|
+
Vocalist: recordingPartyRole.newVocalistRole,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** Builds an `Option<RecordingPartyRoleLevel>` argument for a role constructor. */
|
|
218
|
+
function levelOption(
|
|
219
|
+
tx: Transaction,
|
|
220
|
+
pkg: string,
|
|
221
|
+
level: RecordingRoleLevel | undefined,
|
|
222
|
+
): TransactionArgument {
|
|
223
|
+
const levelType = `${pkg}::recording_party_role::RecordingPartyRoleLevel`;
|
|
224
|
+
if (!level)
|
|
225
|
+
return tx.moveCall({ target: OPTION_NONE, typeArguments: [levelType] });
|
|
226
|
+
const lvl = tx.add(RECORDING_LEVEL_CTOR[level]({ package: pkg }));
|
|
227
|
+
return tx.moveCall({
|
|
228
|
+
target: OPTION_SOME,
|
|
229
|
+
typeArguments: [levelType],
|
|
230
|
+
arguments: [lvl],
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Builds one `CompositionPartyRole` value in the PTB. */
|
|
235
|
+
function buildCompositionRole(
|
|
236
|
+
tx: Transaction,
|
|
237
|
+
pkg: string,
|
|
238
|
+
role: CompositionRole,
|
|
239
|
+
): TransactionObjectArgument {
|
|
240
|
+
if (role.type === "Custom") {
|
|
241
|
+
return tx.add(
|
|
242
|
+
compositionPartyRole.newCustomRole({
|
|
243
|
+
package: pkg,
|
|
244
|
+
arguments: [role.name],
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return tx.add(COMPOSITION_ROLE_CTOR[role.type]({ package: pkg }));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Builds one `RecordingPartyRole` value in the PTB. */
|
|
252
|
+
function buildRecordingRole(
|
|
253
|
+
tx: Transaction,
|
|
254
|
+
pkg: string,
|
|
255
|
+
role: RecordingRole,
|
|
256
|
+
): TransactionObjectArgument {
|
|
257
|
+
switch (role.type) {
|
|
258
|
+
case "ArtistsAndRepertoire":
|
|
259
|
+
return tx.add(
|
|
260
|
+
recordingPartyRole.newArtistsAndRepertoireRole({ package: pkg }),
|
|
261
|
+
);
|
|
262
|
+
case "Copyist":
|
|
263
|
+
return tx.add(recordingPartyRole.newCopyistRole({ package: pkg }));
|
|
264
|
+
case "Instrumentalist":
|
|
265
|
+
return tx.add(
|
|
266
|
+
recordingPartyRole.newInstrumentalistRole({
|
|
267
|
+
package: pkg,
|
|
268
|
+
arguments: [role.instrument, levelOption(tx, pkg, role.level)],
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
case "Custom":
|
|
272
|
+
return tx.add(
|
|
273
|
+
recordingPartyRole.newCustomRole({
|
|
274
|
+
package: pkg,
|
|
275
|
+
arguments: [role.name, levelOption(tx, pkg, role.level)],
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
default:
|
|
279
|
+
return tx.add(
|
|
280
|
+
RECORDING_LEVELED_ROLE_CTOR[role.type]({
|
|
281
|
+
package: pkg,
|
|
282
|
+
arguments: [levelOption(tx, pkg, role.level)],
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Builds one `ReleasePartyRole` value in the PTB. */
|
|
289
|
+
function buildReleaseRole(
|
|
290
|
+
tx: Transaction,
|
|
291
|
+
pkg: string,
|
|
292
|
+
role: ReleaseRole,
|
|
293
|
+
): TransactionObjectArgument {
|
|
294
|
+
const ctor =
|
|
295
|
+
role === "Primary"
|
|
296
|
+
? releasePartyRole.newPrimaryRole
|
|
297
|
+
: releasePartyRole.newFeaturedRole;
|
|
298
|
+
return tx.add(ctor({ package: pkg }));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Client-side validation ────────────────────────────────────────────────────
|
|
302
|
+
//
|
|
303
|
+
// Mirrors the Move aborts so bad params fail fast (and readably) at the writer
|
|
304
|
+
// call site instead of as an on-chain abort code:
|
|
305
|
+
// - miso_credit::credit::new — EEmptyString / EMaxDisplayNameLengthExceeded
|
|
306
|
+
// (display name non-empty, ≤200 BYTES of UTF-8) and ENoRoles / EDuplicateRoles.
|
|
307
|
+
// - composition_credits::add_credit — EMinRolesNotMet / EExceedsMaxRoles (1–5).
|
|
308
|
+
// - recording_credits::add_credit — EMinRolesNotMet / EExceedsMaxRoles (1–10).
|
|
309
|
+
|
|
310
|
+
const MAX_DISPLAY_NAME_BYTES = 200;
|
|
311
|
+
const MAX_COMPOSITION_ROLES = 5;
|
|
312
|
+
const MAX_RECORDING_ROLES = 10;
|
|
313
|
+
|
|
314
|
+
function assertDisplayName(fn: string, displayName: string): void {
|
|
315
|
+
if (displayName.length === 0)
|
|
316
|
+
throw new Error(`${fn}: displayName must not be empty`);
|
|
317
|
+
const byteLength = new TextEncoder().encode(displayName).length;
|
|
318
|
+
if (byteLength > MAX_DISPLAY_NAME_BYTES) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`${fn}: displayName must be at most ${MAX_DISPLAY_NAME_BYTES} bytes of UTF-8 (got ${byteLength})`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Move rejects duplicates by full struct equality, so the duplicate key is the
|
|
326
|
+
// role's complete identity: type + instrument + custom name + level.
|
|
327
|
+
function roleKey(role: CompositionRole | RecordingRole): string {
|
|
328
|
+
return JSON.stringify([
|
|
329
|
+
role.type,
|
|
330
|
+
"instrument" in role ? role.instrument : null,
|
|
331
|
+
"name" in role ? role.name : null,
|
|
332
|
+
"level" in role && role.level !== undefined ? role.level : null,
|
|
333
|
+
]);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function assertRoles(
|
|
337
|
+
fn: string,
|
|
338
|
+
roles: (CompositionRole | RecordingRole)[],
|
|
339
|
+
max: number,
|
|
340
|
+
): void {
|
|
341
|
+
if (roles.length < 1) throw new Error(`${fn}: at least one role is required`);
|
|
342
|
+
if (roles.length > max)
|
|
343
|
+
throw new Error(
|
|
344
|
+
`${fn}: at most ${max} roles are allowed (got ${roles.length})`,
|
|
345
|
+
);
|
|
346
|
+
const seen = new Set<string>();
|
|
347
|
+
for (const role of roles) {
|
|
348
|
+
const key = roleKey(role);
|
|
349
|
+
if (seen.has(key)) throw new Error(`${fn}: duplicate role ${key}`);
|
|
350
|
+
seen.add(key);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Wraps built role values in a `miso_credit::credit::Credit<Role>`. */
|
|
355
|
+
function buildCredit(
|
|
356
|
+
tx: Transaction,
|
|
357
|
+
misoCreditPackageId: string,
|
|
358
|
+
roleType: string,
|
|
359
|
+
displayName: string,
|
|
360
|
+
roleArgs: TransactionObjectArgument[],
|
|
361
|
+
): TransactionObjectArgument {
|
|
362
|
+
const roles = tx.makeMoveVec({ type: roleType, elements: roleArgs });
|
|
363
|
+
return tx.moveCall({
|
|
364
|
+
target: `${misoCreditPackageId}::credit::new`,
|
|
365
|
+
typeArguments: [roleType],
|
|
366
|
+
arguments: [tx.pure.string(displayName), roles],
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Writers ───────────────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
interface AttachCompositionCreditParamsBase {
|
|
373
|
+
/** The `Composition` object to credit on. */
|
|
374
|
+
compositionId: ObjectInput;
|
|
375
|
+
/** The `Party` being credited. */
|
|
376
|
+
partyId: ObjectInput;
|
|
377
|
+
/** Human-readable name for the credit (≤200 bytes, non-empty). */
|
|
378
|
+
displayName: string;
|
|
379
|
+
/** 1–5 writing roles; duplicates are rejected on-chain. */
|
|
380
|
+
roles: CompositionRole[];
|
|
381
|
+
/** The composition's share coin type (the `CompositionShare` phantom). */
|
|
382
|
+
compositionShareType: string;
|
|
383
|
+
/** `composition_credits` package id (roles + `add_credit`). */
|
|
384
|
+
compositionCreditsPackageId: string;
|
|
385
|
+
/** `miso_credit` package id (home of `credit::new`). */
|
|
386
|
+
misoCreditPackageId: string;
|
|
387
|
+
}
|
|
388
|
+
export type AttachCompositionCreditParams =
|
|
389
|
+
AttachCompositionCreditParamsBase & CompositionAuthorityInput;
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Adds a writing credit for a party on a composition. Throws (client-side,
|
|
393
|
+
* mirroring the Move aborts) when `displayName` is empty or over 200 UTF-8
|
|
394
|
+
* bytes, or `roles` is empty, has more than 5 entries, or contains duplicates.
|
|
395
|
+
*/
|
|
396
|
+
export function attachCompositionCredit(
|
|
397
|
+
p: AttachCompositionCreditParams,
|
|
398
|
+
): TxThunk {
|
|
399
|
+
assertDisplayName("attachCompositionCredit", p.displayName);
|
|
400
|
+
assertRoles("attachCompositionCredit", p.roles, MAX_COMPOSITION_ROLES);
|
|
401
|
+
return (tx) => {
|
|
402
|
+
const roleType = `${p.compositionCreditsPackageId}::composition_party_role::CompositionPartyRole`;
|
|
403
|
+
const roleArgs = p.roles.map((r) =>
|
|
404
|
+
buildCompositionRole(tx, p.compositionCreditsPackageId, r),
|
|
405
|
+
);
|
|
406
|
+
const credit = buildCredit(
|
|
407
|
+
tx,
|
|
408
|
+
p.misoCreditPackageId,
|
|
409
|
+
roleType,
|
|
410
|
+
p.displayName,
|
|
411
|
+
roleArgs,
|
|
412
|
+
);
|
|
413
|
+
invokeWithAdminCap(tx, compositionAuthorityOf(p), {
|
|
414
|
+
target: `${p.compositionCreditsPackageId}::composition_credits::add_credit`,
|
|
415
|
+
typeArguments: [p.compositionShareType],
|
|
416
|
+
arguments: [object(tx, p.compositionId), object(tx, p.partyId), credit],
|
|
417
|
+
adminCapIndex: 1,
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
interface AttachRecordingCreditParamsBase {
|
|
423
|
+
/** The `Recording` object to credit on. */
|
|
424
|
+
recordingId: ObjectInput;
|
|
425
|
+
/** The `Party` being credited. */
|
|
426
|
+
partyId: ObjectInput;
|
|
427
|
+
/** Human-readable name for the credit (≤200 bytes, non-empty). */
|
|
428
|
+
displayName: string;
|
|
429
|
+
/** 1–10 production/performance roles; duplicates are rejected on-chain. */
|
|
430
|
+
roles: RecordingRole[];
|
|
431
|
+
/** The recording's own share coin type (the `RecordingShare` phantom). */
|
|
432
|
+
recordingShareType: string;
|
|
433
|
+
/** The parent composition's share coin type (the `CompositionShare` phantom). */
|
|
434
|
+
compositionShareType: string;
|
|
435
|
+
/** `recording_credits` package id (roles + `add_credit`). */
|
|
436
|
+
recordingCreditsPackageId: string;
|
|
437
|
+
/** `miso_credit` package id (home of `credit::new`). */
|
|
438
|
+
misoCreditPackageId: string;
|
|
439
|
+
}
|
|
440
|
+
export type AttachRecordingCreditParams =
|
|
441
|
+
AttachRecordingCreditParamsBase & RecordingAuthorityInput;
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Adds a production/performance credit for a party on a recording. Throws
|
|
445
|
+
* (client-side, mirroring the Move aborts) when `displayName` is empty or over
|
|
446
|
+
* 200 UTF-8 bytes, or `roles` is empty, has more than 10 entries, or contains
|
|
447
|
+
* duplicates (same type + instrument + custom name + level).
|
|
448
|
+
*/
|
|
449
|
+
export function attachRecordingCredit(p: AttachRecordingCreditParams): TxThunk {
|
|
450
|
+
assertDisplayName("attachRecordingCredit", p.displayName);
|
|
451
|
+
assertRoles("attachRecordingCredit", p.roles, MAX_RECORDING_ROLES);
|
|
452
|
+
return (tx) => {
|
|
453
|
+
const roleType = `${p.recordingCreditsPackageId}::recording_party_role::RecordingPartyRole`;
|
|
454
|
+
const roleArgs = p.roles.map((r) =>
|
|
455
|
+
buildRecordingRole(tx, p.recordingCreditsPackageId, r),
|
|
456
|
+
);
|
|
457
|
+
const credit = buildCredit(
|
|
458
|
+
tx,
|
|
459
|
+
p.misoCreditPackageId,
|
|
460
|
+
roleType,
|
|
461
|
+
p.displayName,
|
|
462
|
+
roleArgs,
|
|
463
|
+
);
|
|
464
|
+
invokeWithAdminCap(tx, recordingAuthorityOf(p), {
|
|
465
|
+
target: `${p.recordingCreditsPackageId}::recording_credits::add_credit`,
|
|
466
|
+
typeArguments: [p.recordingShareType, p.compositionShareType],
|
|
467
|
+
arguments: [object(tx, p.recordingId), object(tx, p.partyId), credit],
|
|
468
|
+
adminCapIndex: 1,
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
interface AddRecordingArtistParamsBase {
|
|
474
|
+
/** The `Recording` object. */
|
|
475
|
+
recordingId: ObjectInput;
|
|
476
|
+
/** The `Party` to designate. Must already be credited on the recording. */
|
|
477
|
+
partyId: ObjectInput;
|
|
478
|
+
/** The recording's own share coin type (the `RecordingShare` phantom). */
|
|
479
|
+
recordingShareType: string;
|
|
480
|
+
/** The parent composition's share coin type (the `CompositionShare` phantom). */
|
|
481
|
+
compositionShareType: string;
|
|
482
|
+
/** `recording_credits` package id. */
|
|
483
|
+
recordingCreditsPackageId: string;
|
|
484
|
+
}
|
|
485
|
+
export type AddRecordingArtistParams =
|
|
486
|
+
AddRecordingArtistParamsBase & RecordingAuthorityInput;
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Designates an already-credited party as a primary artist on a recording. The
|
|
490
|
+
* party must be credited first (via `attachRecordingCredit`) and not already a
|
|
491
|
+
* primary or featured artist.
|
|
492
|
+
*/
|
|
493
|
+
export function addRecordingPrimaryArtist(
|
|
494
|
+
p: AddRecordingArtistParams,
|
|
495
|
+
): TxThunk {
|
|
496
|
+
return (tx) => {
|
|
497
|
+
invokeWithAdminCap(tx, recordingAuthorityOf(p), {
|
|
498
|
+
target: `${p.recordingCreditsPackageId}::recording_credits::add_primary_artist`,
|
|
499
|
+
typeArguments: [p.recordingShareType, p.compositionShareType],
|
|
500
|
+
arguments: [object(tx, p.recordingId), object(tx, p.partyId)],
|
|
501
|
+
adminCapIndex: 1,
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Designates an already-credited party as a featured artist on a recording. The
|
|
508
|
+
* party must be credited first (via `attachRecordingCredit`) and not already a
|
|
509
|
+
* primary or featured artist.
|
|
510
|
+
*/
|
|
511
|
+
export function addRecordingFeaturedArtist(
|
|
512
|
+
p: AddRecordingArtistParams,
|
|
513
|
+
): TxThunk {
|
|
514
|
+
return (tx) => {
|
|
515
|
+
invokeWithAdminCap(tx, recordingAuthorityOf(p), {
|
|
516
|
+
target: `${p.recordingCreditsPackageId}::recording_credits::add_featured_artist`,
|
|
517
|
+
typeArguments: [p.recordingShareType, p.compositionShareType],
|
|
518
|
+
arguments: [object(tx, p.recordingId), object(tx, p.partyId)],
|
|
519
|
+
adminCapIndex: 1,
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
interface AddReleaseCreditParamsBase {
|
|
525
|
+
/** The `Release` object to credit on. */
|
|
526
|
+
releaseId: ObjectInput;
|
|
527
|
+
/** The `Party` being credited. */
|
|
528
|
+
partyId: ObjectInput;
|
|
529
|
+
/** Human-readable name for the credit (≤200 bytes, non-empty). */
|
|
530
|
+
displayName: string;
|
|
531
|
+
/** The single billing role: `"Primary"` or `"Featured"`. */
|
|
532
|
+
role: ReleaseRole;
|
|
533
|
+
/** `release_credits` package id (roles + `add_credit`). */
|
|
534
|
+
releaseCreditsPackageId: string;
|
|
535
|
+
/** `miso_credit` package id (home of `credit::new`). */
|
|
536
|
+
misoCreditPackageId: string;
|
|
537
|
+
}
|
|
538
|
+
export type AddReleaseCreditParams = AddReleaseCreditParamsBase & ReleaseAuthorityInput;
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Adds a top-line billing credit (a single role) for a party on a release.
|
|
542
|
+
* Throws (client-side, mirroring the Move aborts) when `displayName` is empty
|
|
543
|
+
* or over 200 UTF-8 bytes. The single role is structural, so no role checks.
|
|
544
|
+
*/
|
|
545
|
+
export function addReleaseCredit(p: AddReleaseCreditParams): TxThunk {
|
|
546
|
+
assertDisplayName("addReleaseCredit", p.displayName);
|
|
547
|
+
return (tx) => {
|
|
548
|
+
const roleType = `${p.releaseCreditsPackageId}::release_party_role::ReleasePartyRole`;
|
|
549
|
+
const roleArg = buildReleaseRole(tx, p.releaseCreditsPackageId, p.role);
|
|
550
|
+
const credit = buildCredit(
|
|
551
|
+
tx,
|
|
552
|
+
p.misoCreditPackageId,
|
|
553
|
+
roleType,
|
|
554
|
+
p.displayName,
|
|
555
|
+
[roleArg],
|
|
556
|
+
);
|
|
557
|
+
invokeWithAdminCap(tx, releaseAuthorityOf(p), {
|
|
558
|
+
target: `${p.releaseCreditsPackageId}::release_credits::add_credit`,
|
|
559
|
+
arguments: [object(tx, p.releaseId), object(tx, p.partyId), credit],
|
|
560
|
+
adminCapIndex: 1,
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Reads ─────────────────────────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
/** A normalized credit: the party, its display name, and its role labels. */
|
|
568
|
+
export interface CreditView {
|
|
569
|
+
/** Object id of the credited `Party`. */
|
|
570
|
+
partyId: string;
|
|
571
|
+
/** Human-readable name for the credit. */
|
|
572
|
+
displayName: string;
|
|
573
|
+
/**
|
|
574
|
+
* Role labels (the `name()` semantics). For a recording `Instrumentalist` the
|
|
575
|
+
* instrument is included (`"Instrumentalist: Guitar"`); a recording role's level
|
|
576
|
+
* is appended in parentheses (`"Producer (Lead)"`).
|
|
577
|
+
*/
|
|
578
|
+
roles: string[];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** A recording's credits, plus its primary/featured artist party ids. */
|
|
582
|
+
export interface RecordingCreditsView {
|
|
583
|
+
credits: CreditView[];
|
|
584
|
+
primaryArtistIds: string[];
|
|
585
|
+
featuredArtistIds: string[];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// A parsed closed-enum value: `{ $kind, [variant]: payload }`. Unit variants carry
|
|
589
|
+
// `true`; data variants carry their parsed payload.
|
|
590
|
+
type ParsedEnum = { $kind: string } & Record<string, unknown>;
|
|
591
|
+
type ParsedLevel = ParsedEnum | null;
|
|
592
|
+
|
|
593
|
+
// Credits are stored as a dynamic field on the work's UID under the extension's
|
|
594
|
+
// `ExtensionKey()` — a positional struct with no fields, so Move's implicit
|
|
595
|
+
// `dummy_field: bool` serializes to a single `false` byte. The stored object is a
|
|
596
|
+
// `Field { id, name: <Key>, value: <Credits> }`.
|
|
597
|
+
const CompositionCreditsField = bcs.struct("Field", {
|
|
598
|
+
id: bcs.Address,
|
|
599
|
+
name: compositionCredits.ExtensionKey,
|
|
600
|
+
value: compositionCredits.CompositionCredits,
|
|
601
|
+
});
|
|
602
|
+
const RecordingCreditsField = bcs.struct("Field", {
|
|
603
|
+
id: bcs.Address,
|
|
604
|
+
name: recordingCredits.ExtensionKey,
|
|
605
|
+
value: recordingCredits.RecordingCredits,
|
|
606
|
+
});
|
|
607
|
+
const ReleaseCreditsField = bcs.struct("Field", {
|
|
608
|
+
id: bcs.Address,
|
|
609
|
+
name: releaseCredits.ExtensionKey,
|
|
610
|
+
value: releaseCredits.ReleaseCredits,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const COMPOSITION_CREDITS_KEY_BYTES = compositionCredits.ExtensionKey.serialize(
|
|
614
|
+
[false],
|
|
615
|
+
).toBytes();
|
|
616
|
+
const RECORDING_CREDITS_KEY_BYTES = recordingCredits.ExtensionKey.serialize([
|
|
617
|
+
false,
|
|
618
|
+
]).toBytes();
|
|
619
|
+
const RELEASE_CREDITS_KEY_BYTES = releaseCredits.ExtensionKey.serialize([
|
|
620
|
+
false,
|
|
621
|
+
]).toBytes();
|
|
622
|
+
|
|
623
|
+
type CreditFieldKind =
|
|
624
|
+
| "composition"
|
|
625
|
+
| "recording"
|
|
626
|
+
| "release";
|
|
627
|
+
|
|
628
|
+
interface CreditFieldTarget {
|
|
629
|
+
workId: string;
|
|
630
|
+
kind: CreditFieldKind;
|
|
631
|
+
fieldId: string;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Fetch many derived credit fields through one Core bulk request. */
|
|
635
|
+
async function fetchCreditFields(
|
|
636
|
+
client: ClientWithCoreApi,
|
|
637
|
+
targets: readonly CreditFieldTarget[],
|
|
638
|
+
): Promise<Map<string, Uint8Array>> {
|
|
639
|
+
if (targets.length === 0) return new Map();
|
|
640
|
+
const { objects } = await client.core.getObjects({
|
|
641
|
+
objectIds: targets.map((target) => target.fieldId),
|
|
642
|
+
include: { content: true },
|
|
643
|
+
});
|
|
644
|
+
const contents = new Map<string, Uint8Array>();
|
|
645
|
+
objects.forEach((object, index) => {
|
|
646
|
+
if (object instanceof Error || !object.content) return;
|
|
647
|
+
const target = targets[index];
|
|
648
|
+
if (target) contents.set(target.fieldId, object.content);
|
|
649
|
+
});
|
|
650
|
+
return contents;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function compositionCreditTargets(
|
|
654
|
+
compositionIds: readonly string[],
|
|
655
|
+
packageId: string,
|
|
656
|
+
): CreditFieldTarget[] {
|
|
657
|
+
return compositionIds.map((workId) => ({
|
|
658
|
+
workId,
|
|
659
|
+
kind: "composition" as const,
|
|
660
|
+
fieldId: deriveDynamicFieldID(
|
|
661
|
+
workId,
|
|
662
|
+
`${packageId}::composition_credits::ExtensionKey`,
|
|
663
|
+
COMPOSITION_CREDITS_KEY_BYTES,
|
|
664
|
+
),
|
|
665
|
+
}));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function recordingCreditTargets(
|
|
669
|
+
recordingIds: readonly string[],
|
|
670
|
+
packageId: string,
|
|
671
|
+
): CreditFieldTarget[] {
|
|
672
|
+
return recordingIds.map((workId) => ({
|
|
673
|
+
workId,
|
|
674
|
+
kind: "recording" as const,
|
|
675
|
+
fieldId: deriveDynamicFieldID(
|
|
676
|
+
workId,
|
|
677
|
+
`${packageId}::recording_credits::ExtensionKey`,
|
|
678
|
+
RECORDING_CREDITS_KEY_BYTES,
|
|
679
|
+
),
|
|
680
|
+
}));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function releaseCreditTargets(
|
|
684
|
+
releaseIds: readonly string[],
|
|
685
|
+
packageId: string,
|
|
686
|
+
): CreditFieldTarget[] {
|
|
687
|
+
return releaseIds.map((workId) => ({
|
|
688
|
+
workId,
|
|
689
|
+
kind: "release",
|
|
690
|
+
fieldId: releaseCreditsFieldId(workId, packageId),
|
|
691
|
+
}));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Deterministic dynamic-field id for a release's billing credits. */
|
|
695
|
+
export function releaseCreditsFieldId(
|
|
696
|
+
releaseId: string,
|
|
697
|
+
releaseCreditsPackageId: string,
|
|
698
|
+
): string {
|
|
699
|
+
return deriveDynamicFieldID(
|
|
700
|
+
releaseId,
|
|
701
|
+
`${releaseCreditsPackageId}::release_credits::ExtensionKey`,
|
|
702
|
+
RELEASE_CREDITS_KEY_BYTES,
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// The credits container shape, checked structurally against the parse output of
|
|
707
|
+
// the generated bindings (their `$inferType`): the readers below pass
|
|
708
|
+
// `<X>CreditsField.parse(...).value.credits` in WITHOUT casting, so a codegen
|
|
709
|
+
// field rename (`contents`/`key`/`value`/`display_name`/`roles`, or the
|
|
710
|
+
// `credits`/`primary_artist_ids`/`featured_artist_ids` fields at the call
|
|
711
|
+
// sites) fails typecheck here instead of silently misparsing.
|
|
712
|
+
type ParsedCreditsMap<Role> = {
|
|
713
|
+
contents: { key: string; value: { display_name: string; roles: Role[] } }[];
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
function creditViews<Role extends ParsedEnum>(
|
|
717
|
+
credits: ParsedCreditsMap<Role>,
|
|
718
|
+
roleLabel: (role: Role) => string,
|
|
719
|
+
): CreditView[] {
|
|
720
|
+
return credits.contents.map((entry) => ({
|
|
721
|
+
partyId: entry.key,
|
|
722
|
+
displayName: entry.value.display_name,
|
|
723
|
+
roles: entry.value.roles.map(roleLabel),
|
|
724
|
+
}));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// `name()` semantics: the `$kind` is the canonical PascalCase token; `Custom`
|
|
728
|
+
// carries the user-supplied name in its payload.
|
|
729
|
+
function compositionRoleLabel(role: ParsedEnum): string {
|
|
730
|
+
return role.$kind === "Custom" ? (role.Custom as string) : role.$kind;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function releaseRoleLabel(role: ParsedEnum): string {
|
|
734
|
+
return role.$kind;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** Parse a fetched release-credit dynamic field. */
|
|
738
|
+
export function parseReleaseCreditsContent(content: Uint8Array): CreditView[] {
|
|
739
|
+
return creditViews(
|
|
740
|
+
ReleaseCreditsField.parse(content).value.credits,
|
|
741
|
+
releaseRoleLabel,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// `name()` semantics plus the recording extras: the instrument (for
|
|
746
|
+
// `Instrumentalist`) and the optional level, both surfaced into the label.
|
|
747
|
+
function recordingRoleLabel(role: ParsedEnum): string {
|
|
748
|
+
let base: string;
|
|
749
|
+
let level: ParsedLevel = null;
|
|
750
|
+
switch (role.$kind) {
|
|
751
|
+
case "Instrumentalist": {
|
|
752
|
+
const [instrument, lvl] = role.Instrumentalist as [string, ParsedLevel];
|
|
753
|
+
base = `Instrumentalist: ${instrument}`;
|
|
754
|
+
level = lvl;
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case "Custom": {
|
|
758
|
+
const [name, lvl] = role.Custom as [string, ParsedLevel];
|
|
759
|
+
base = name;
|
|
760
|
+
level = lvl;
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
case "ArtistsAndRepertoire":
|
|
764
|
+
case "Copyist":
|
|
765
|
+
base = role.$kind;
|
|
766
|
+
break;
|
|
767
|
+
default: {
|
|
768
|
+
// A leveled variant's payload is `Option<Level>`: `None` parses to `true`
|
|
769
|
+
// (the unit-payload convention), `Some(level)` to the level enum object.
|
|
770
|
+
base = role.$kind;
|
|
771
|
+
const payload = role[role.$kind];
|
|
772
|
+
level =
|
|
773
|
+
payload && typeof payload === "object" && "$kind" in payload
|
|
774
|
+
? (payload as ParsedEnum)
|
|
775
|
+
: null;
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return level ? `${base} (${level.$kind})` : base;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Reads a composition's writing credits (the `composition_credits` extension), or
|
|
784
|
+
* `null` if no credits field is attached.
|
|
785
|
+
*/
|
|
786
|
+
export async function getCompositionCredits(
|
|
787
|
+
client: ClientWithCoreApi,
|
|
788
|
+
compositionId: string,
|
|
789
|
+
compositionCreditsPackageId: string,
|
|
790
|
+
): Promise<CreditView[] | null> {
|
|
791
|
+
return (
|
|
792
|
+
(
|
|
793
|
+
await getCompositionCreditsByIds(
|
|
794
|
+
client,
|
|
795
|
+
[compositionId],
|
|
796
|
+
compositionCreditsPackageId,
|
|
797
|
+
)
|
|
798
|
+
)[compositionId] ?? null
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** Composition credits for many works in one Core bulk request. */
|
|
803
|
+
export async function getCompositionCreditsByIds(
|
|
804
|
+
client: ClientWithCoreApi,
|
|
805
|
+
compositionIdsInput: readonly string[],
|
|
806
|
+
compositionCreditsPackageId: string,
|
|
807
|
+
): Promise<Partial<Record<string, CreditView[]>>> {
|
|
808
|
+
const compositionIds = [...new Set(compositionIdsInput)];
|
|
809
|
+
const targets = compositionCreditTargets(
|
|
810
|
+
compositionIds,
|
|
811
|
+
compositionCreditsPackageId,
|
|
812
|
+
);
|
|
813
|
+
const contents = await fetchCreditFields(client, targets);
|
|
814
|
+
const out: Partial<Record<string, CreditView[]>> = {};
|
|
815
|
+
for (const target of targets) {
|
|
816
|
+
const content = contents.get(target.fieldId);
|
|
817
|
+
if (content) {
|
|
818
|
+
out[target.workId] = creditViews(
|
|
819
|
+
CompositionCreditsField.parse(content).value.credits,
|
|
820
|
+
compositionRoleLabel,
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return out;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Reads a recording's credits plus its primary/featured artist party ids (the
|
|
829
|
+
* `recording_credits` extension), or `null` if no credits field is attached.
|
|
830
|
+
*/
|
|
831
|
+
export async function getRecordingCredits(
|
|
832
|
+
client: ClientWithCoreApi,
|
|
833
|
+
recordingId: string,
|
|
834
|
+
recordingCreditsPackageId: string,
|
|
835
|
+
): Promise<RecordingCreditsView | null> {
|
|
836
|
+
return (
|
|
837
|
+
(
|
|
838
|
+
await getRecordingCreditsByIds(
|
|
839
|
+
client,
|
|
840
|
+
[recordingId],
|
|
841
|
+
recordingCreditsPackageId,
|
|
842
|
+
)
|
|
843
|
+
)[recordingId] ?? null
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Recording credits for many works in one Core bulk request. */
|
|
848
|
+
export async function getRecordingCreditsByIds(
|
|
849
|
+
client: ClientWithCoreApi,
|
|
850
|
+
recordingIdsInput: readonly string[],
|
|
851
|
+
recordingCreditsPackageId: string,
|
|
852
|
+
): Promise<Partial<Record<string, RecordingCreditsView>>> {
|
|
853
|
+
const recordingIds = [...new Set(recordingIdsInput)];
|
|
854
|
+
const targets = recordingCreditTargets(
|
|
855
|
+
recordingIds,
|
|
856
|
+
recordingCreditsPackageId,
|
|
857
|
+
);
|
|
858
|
+
const contents = await fetchCreditFields(client, targets);
|
|
859
|
+
const out: Partial<Record<string, RecordingCreditsView>> = {};
|
|
860
|
+
for (const target of targets) {
|
|
861
|
+
const content = contents.get(target.fieldId);
|
|
862
|
+
if (!content) continue;
|
|
863
|
+
const value = RecordingCreditsField.parse(content).value;
|
|
864
|
+
out[target.workId] = {
|
|
865
|
+
credits: creditViews(value.credits, recordingRoleLabel),
|
|
866
|
+
primaryArtistIds: value.primary_artist_ids.contents,
|
|
867
|
+
featuredArtistIds: value.featured_artist_ids.contents,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
return out;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Reads a release's top-line billing credits (the `release_credits` extension), or
|
|
875
|
+
* `null` if no credits field is attached. Each credit has exactly one role.
|
|
876
|
+
*/
|
|
877
|
+
export async function getReleaseCredits(
|
|
878
|
+
client: ClientWithCoreApi,
|
|
879
|
+
releaseId: string,
|
|
880
|
+
releaseCreditsPackageId: string,
|
|
881
|
+
): Promise<CreditView[] | null> {
|
|
882
|
+
return (
|
|
883
|
+
(
|
|
884
|
+
await getReleaseCreditsByIds(client, [releaseId], releaseCreditsPackageId)
|
|
885
|
+
)[releaseId] ?? null
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** Release billing credits for many releases in one Core bulk request. */
|
|
890
|
+
export async function getReleaseCreditsByIds(
|
|
891
|
+
client: ClientWithCoreApi,
|
|
892
|
+
releaseIdsInput: readonly string[],
|
|
893
|
+
releaseCreditsPackageId: string,
|
|
894
|
+
): Promise<Partial<Record<string, CreditView[]>>> {
|
|
895
|
+
const releaseIds = [...new Set(releaseIdsInput)];
|
|
896
|
+
const targets = releaseCreditTargets(releaseIds, releaseCreditsPackageId);
|
|
897
|
+
const contents = await fetchCreditFields(client, targets);
|
|
898
|
+
const out: Partial<Record<string, CreditView[]>> = {};
|
|
899
|
+
for (const target of targets) {
|
|
900
|
+
const content = contents.get(target.fieldId);
|
|
901
|
+
if (content) {
|
|
902
|
+
out[target.workId] = parseReleaseCreditsContent(content);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return out;
|
|
906
|
+
}
|