@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
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// Catalog reads: pressings, releases, covers, credits, tracklists, and the
|
|
5
|
+
// Discover shelf. Everything here is PUBLIC — no address is involved and no two
|
|
6
|
+
// callers get different answers, which is what makes this layer edge-cacheable.
|
|
7
|
+
//
|
|
8
|
+
// The composition of these reads is the point. A pressing page used to cost the
|
|
9
|
+
// browser five sequential round-trips to the chain (pressing → release → cover →
|
|
10
|
+
// credits → recordings); the same work happens once here, inside one datacenter,
|
|
11
|
+
// and every subsequent visitor is served from cache.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
deriveListingId,
|
|
15
|
+
getListing,
|
|
16
|
+
getPressing,
|
|
17
|
+
getRecord,
|
|
18
|
+
getSale,
|
|
19
|
+
type ListingView as ContractListingView,
|
|
20
|
+
type PressingView as ContractPressingView,
|
|
21
|
+
} from "../pressing.ts";
|
|
22
|
+
import { requireRecordSalesDeployment } from "../deployments.ts";
|
|
23
|
+
import {
|
|
24
|
+
getReleaseCoversByIds,
|
|
25
|
+
parseReleaseCoverContent,
|
|
26
|
+
releaseCoverFieldId,
|
|
27
|
+
type ReleaseCoverView,
|
|
28
|
+
type CoverImageRef,
|
|
29
|
+
} from "../cover.ts";
|
|
30
|
+
import {
|
|
31
|
+
getReleaseCreditsByIds,
|
|
32
|
+
parseReleaseCreditsContent,
|
|
33
|
+
releaseCreditsFieldId,
|
|
34
|
+
type CreditView,
|
|
35
|
+
} from "../credits.ts";
|
|
36
|
+
import {
|
|
37
|
+
parseReleaseKindContent,
|
|
38
|
+
releaseKindFieldId,
|
|
39
|
+
} from "../release-extensions.ts";
|
|
40
|
+
import { getRecordingMasterReferencesByIds } from "../recording-extensions.ts";
|
|
41
|
+
import {
|
|
42
|
+
getReleaseMixReferences,
|
|
43
|
+
walrusBlobIdFromU256,
|
|
44
|
+
} from "../mix.ts";
|
|
45
|
+
import { getTrackCreditsByRecordingIds } from "../catalog.ts";
|
|
46
|
+
import { getReleaseById, getReleasesByIds, isNotFound } from "@misofm/protocol";
|
|
47
|
+
import type { Release } from "@misofm/protocol";
|
|
48
|
+
import type { MisoClient } from "./client.ts";
|
|
49
|
+
import { getRecordingTitles, parseReleaseObject } from "./works.ts";
|
|
50
|
+
import { int } from "./internal/scalars.ts";
|
|
51
|
+
import {
|
|
52
|
+
quiltPatchId,
|
|
53
|
+
u256ToB64Url,
|
|
54
|
+
walrusBlobReadUrl,
|
|
55
|
+
} from "./internal/walrus.ts";
|
|
56
|
+
import type {
|
|
57
|
+
Cover,
|
|
58
|
+
CoverImage,
|
|
59
|
+
Credit,
|
|
60
|
+
Currency,
|
|
61
|
+
DiscoverItem,
|
|
62
|
+
ListingView,
|
|
63
|
+
PressingDetail,
|
|
64
|
+
PressingPreview,
|
|
65
|
+
PressingView,
|
|
66
|
+
Price,
|
|
67
|
+
RecordAlbum,
|
|
68
|
+
ReleaseDetail,
|
|
69
|
+
SaleDetail,
|
|
70
|
+
SaleView,
|
|
71
|
+
TrackCredits,
|
|
72
|
+
TrackView,
|
|
73
|
+
WorkState,
|
|
74
|
+
} from "./types.ts";
|
|
75
|
+
|
|
76
|
+
// ── Walrus URLs ──────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/** Aggregator URL for a cover image ref, whichever Walrus variant it is. */
|
|
79
|
+
function imageUrl(aggregator: string, ref: CoverImageRef): string {
|
|
80
|
+
const base = aggregator.replace(/\/$/, "");
|
|
81
|
+
if (ref.kind === "blob") return walrusBlobReadUrl(base, ref.blobId);
|
|
82
|
+
const patch = quiltPatchId(
|
|
83
|
+
ref.quiltId,
|
|
84
|
+
ref.version,
|
|
85
|
+
ref.startIndex,
|
|
86
|
+
ref.endIndex,
|
|
87
|
+
);
|
|
88
|
+
return `${base}/v1/blobs/by-quilt-patch-id/${patch}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toCover(
|
|
92
|
+
aggregator: string,
|
|
93
|
+
view: ReleaseCoverView | null,
|
|
94
|
+
): Cover | null {
|
|
95
|
+
if (!view) return null;
|
|
96
|
+
const image = (ref: CoverImageRef): CoverImage => ({
|
|
97
|
+
kind: ref.kind,
|
|
98
|
+
url: imageUrl(aggregator, ref),
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
still: image(view.still),
|
|
102
|
+
animated: view.animated ? image(view.animated) : null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Currency ─────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Symbol + decimals for a coin type. FakeUsd (the testnet dollar) is 6dp; SUI is
|
|
110
|
+
* 9dp; anything else falls back to its module name at 9dp, which is the Sui
|
|
111
|
+
* default and the honest guess for a coin we have no table entry for.
|
|
112
|
+
*/
|
|
113
|
+
export function currencyInfo(type: string | null): Currency {
|
|
114
|
+
if (!type) return { type: null, symbol: "COIN", decimals: 9 };
|
|
115
|
+
if (/::fakeusd::/i.test(type))
|
|
116
|
+
return { type, symbol: "FAKEUSD", decimals: 6 };
|
|
117
|
+
if (type === "0x2::sui::SUI") return { type, symbol: "SUI", decimals: 9 };
|
|
118
|
+
return {
|
|
119
|
+
type,
|
|
120
|
+
symbol: (type.split("::").pop() ?? "COIN").toUpperCase(),
|
|
121
|
+
decimals: 9,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Projections ──────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
function toPressingView(pressing: ContractPressingView): PressingView {
|
|
128
|
+
return {
|
|
129
|
+
id: pressing.id,
|
|
130
|
+
releaseId: pressing.releaseId,
|
|
131
|
+
edition: pressing.edition,
|
|
132
|
+
supply: pressing.supply,
|
|
133
|
+
maxSupply: pressing.maxSupply,
|
|
134
|
+
distributors: [...pressing.distributors],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toListingView(listing: ContractListingView): ListingView {
|
|
139
|
+
const price: Price = {
|
|
140
|
+
kind: listing.pricing.kind,
|
|
141
|
+
amount: listing.pricing.amount,
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
id: listing.id,
|
|
145
|
+
pressingId: listing.pressingId,
|
|
146
|
+
releaseId: listing.releaseId,
|
|
147
|
+
pricing: price,
|
|
148
|
+
currency: currencyInfo(listing.currencyType),
|
|
149
|
+
state: listing.state,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toSaleView(
|
|
154
|
+
pressing: ContractPressingView,
|
|
155
|
+
listing: ContractListingView,
|
|
156
|
+
): SaleView {
|
|
157
|
+
return { pressing: toPressingView(pressing), listing: toListingView(listing) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** One permanent pressing, projected to the JSON-safe read boundary. */
|
|
161
|
+
export async function getPressingView(
|
|
162
|
+
client: MisoClient,
|
|
163
|
+
pressingId: string,
|
|
164
|
+
): Promise<PressingView | null> {
|
|
165
|
+
const sales = requireRecordSalesDeployment(client.config.recordSales);
|
|
166
|
+
const pressing = await getPressing(
|
|
167
|
+
client.protocol,
|
|
168
|
+
pressingId,
|
|
169
|
+
sales.recordPackageId,
|
|
170
|
+
);
|
|
171
|
+
return pressing ? toPressingView(pressing) : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** One currency-specific listing derived from its permanent pressing. */
|
|
175
|
+
export async function getListingView(
|
|
176
|
+
client: MisoClient,
|
|
177
|
+
pressingId: string,
|
|
178
|
+
currencyType: string,
|
|
179
|
+
): Promise<ListingView | null> {
|
|
180
|
+
const sales = requireRecordSalesDeployment(client.config.recordSales);
|
|
181
|
+
const listingId = deriveListingId(pressingId, currencyType, sales.recordShopPackageId);
|
|
182
|
+
const listing = await getListing(client.protocol, listingId, sales.recordShopPackageId);
|
|
183
|
+
return listing ? toListingView(listing) : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function toWorkState(state: Release["state"]): WorkState {
|
|
187
|
+
return state.type === "Published"
|
|
188
|
+
? { type: "Published", timestampMs: state.timestampMs }
|
|
189
|
+
: { type: "Initialized" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function toCredits(credits: CreditView[] | null): Credit[] {
|
|
193
|
+
return (credits ?? []).map((c) => ({
|
|
194
|
+
partyId: c.partyId,
|
|
195
|
+
displayName: c.displayName,
|
|
196
|
+
roles: [...c.roles],
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The release's PRIMARY credits, in chain order — a release's artist line. */
|
|
201
|
+
export function primaryArtistNames(credits: Credit[]): string[] {
|
|
202
|
+
return credits
|
|
203
|
+
.filter((c) => c.roles.includes("Primary"))
|
|
204
|
+
.map((c) => c.displayName);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Number the protocol's flat tracklist. Display grouping such as discs belongs
|
|
209
|
+
* to metadata extensions and can be layered onto this projection later.
|
|
210
|
+
*/
|
|
211
|
+
export function toTracks(
|
|
212
|
+
release: Release,
|
|
213
|
+
titles: Record<string, string>,
|
|
214
|
+
masterBlobIds: Partial<Record<string, string>>,
|
|
215
|
+
mixBlobIds: Partial<Record<number, string>> = {},
|
|
216
|
+
): TrackView[] {
|
|
217
|
+
return release.tracks.map((track, index) => {
|
|
218
|
+
const masterBlobId = masterBlobIds[track.recordingId];
|
|
219
|
+
const mixBlobId = mixBlobIds[index];
|
|
220
|
+
return {
|
|
221
|
+
no: `${index + 1}`,
|
|
222
|
+
title: titles[track.recordingId] ?? "Untitled",
|
|
223
|
+
recordingId: track.recordingId,
|
|
224
|
+
compositionId: track.compositionId,
|
|
225
|
+
splitBps: int(track.splitBps.value),
|
|
226
|
+
disc: 1,
|
|
227
|
+
...(masterBlobId ? { masterBlobId } : {}),
|
|
228
|
+
...(mixBlobId ? { mixBlobId } : {}),
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Cover ────────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/** A release's cover from the configured `release_cover_art` package. */
|
|
236
|
+
export async function readReleaseCover(
|
|
237
|
+
client: MisoClient,
|
|
238
|
+
releaseId: string,
|
|
239
|
+
): Promise<Cover | null> {
|
|
240
|
+
const { releaseCoverArt } = client.config.protocol;
|
|
241
|
+
const covers = await getReleaseCoversByIds(
|
|
242
|
+
client.protocol,
|
|
243
|
+
[releaseId],
|
|
244
|
+
releaseCoverArt,
|
|
245
|
+
).catch(() => ({}) as Partial<Record<string, ReleaseCoverView>>);
|
|
246
|
+
return toCover(client.config.walrusAggregatorUrl, covers[releaseId] ?? null);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export type ReleaseResourceInclude = "cover" | "credits" | "kind";
|
|
250
|
+
|
|
251
|
+
export interface ReleaseResources {
|
|
252
|
+
release: Release;
|
|
253
|
+
cover?: Cover | null;
|
|
254
|
+
credits?: Credit[];
|
|
255
|
+
kind?: string | null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A release plus selected derived resources in one heterogeneous Core batch.
|
|
260
|
+
* Every extension id is deterministic, so separate object calls only add
|
|
261
|
+
* network round trips without discovering anything new.
|
|
262
|
+
*/
|
|
263
|
+
export async function getReleaseResources(
|
|
264
|
+
client: MisoClient,
|
|
265
|
+
releaseId: string,
|
|
266
|
+
include: readonly ReleaseResourceInclude[] = [],
|
|
267
|
+
): Promise<ReleaseResources> {
|
|
268
|
+
const wantsCover = include.includes("cover");
|
|
269
|
+
const wantsCredits = include.includes("credits");
|
|
270
|
+
const wantsKind = include.includes("kind");
|
|
271
|
+
const coverFieldIds = wantsCover
|
|
272
|
+
? [releaseCoverFieldId(releaseId, client.config.protocol.releaseCoverArt)]
|
|
273
|
+
: [];
|
|
274
|
+
const creditsFieldId = wantsCredits
|
|
275
|
+
? releaseCreditsFieldId(releaseId, client.config.protocol.releaseCredits)
|
|
276
|
+
: null;
|
|
277
|
+
const kindFieldId = wantsKind
|
|
278
|
+
? releaseKindFieldId(releaseId, client.config.protocol.releaseKind)
|
|
279
|
+
: null;
|
|
280
|
+
const objectIds = [
|
|
281
|
+
releaseId,
|
|
282
|
+
...coverFieldIds,
|
|
283
|
+
...(creditsFieldId ? [creditsFieldId] : []),
|
|
284
|
+
...(kindFieldId ? [kindFieldId] : []),
|
|
285
|
+
];
|
|
286
|
+
const { objects } = await client.protocol.core.getObjects({
|
|
287
|
+
objectIds,
|
|
288
|
+
include: { content: true },
|
|
289
|
+
});
|
|
290
|
+
const releaseObject = objects[0];
|
|
291
|
+
if (!releaseObject) throw new Error(`Release not found: ${releaseId}`);
|
|
292
|
+
if (releaseObject instanceof Error) throw releaseObject;
|
|
293
|
+
if (!releaseObject.content)
|
|
294
|
+
throw new Error(`Release has no content: ${releaseId}`);
|
|
295
|
+
const release = parseReleaseObject(
|
|
296
|
+
releaseObject.objectId,
|
|
297
|
+
releaseObject.content,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
let cover: Cover | null | undefined;
|
|
301
|
+
if (wantsCover) {
|
|
302
|
+
let view: ReleaseCoverView | null = null;
|
|
303
|
+
for (let index = 0; index < coverFieldIds.length; index += 1) {
|
|
304
|
+
const object = objects[index + 1];
|
|
305
|
+
if (!object || object instanceof Error || !object.content) continue;
|
|
306
|
+
try {
|
|
307
|
+
view = parseReleaseCoverContent(object.content);
|
|
308
|
+
} catch {
|
|
309
|
+
view = null;
|
|
310
|
+
}
|
|
311
|
+
if (view) break;
|
|
312
|
+
}
|
|
313
|
+
cover = toCover(client.config.walrusAggregatorUrl, view);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let credits: Credit[] | undefined;
|
|
317
|
+
if (wantsCredits) {
|
|
318
|
+
const object = objects[1 + coverFieldIds.length];
|
|
319
|
+
let view: CreditView[] = [];
|
|
320
|
+
if (object && !(object instanceof Error) && object.content) {
|
|
321
|
+
try {
|
|
322
|
+
view = parseReleaseCreditsContent(object.content);
|
|
323
|
+
} catch {
|
|
324
|
+
view = [];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
credits = toCredits(view);
|
|
328
|
+
}
|
|
329
|
+
let kind: string | null | undefined;
|
|
330
|
+
if (wantsKind) {
|
|
331
|
+
const object = objects[
|
|
332
|
+
1 + coverFieldIds.length + (creditsFieldId ? 1 : 0)
|
|
333
|
+
];
|
|
334
|
+
kind =
|
|
335
|
+
object && !(object instanceof Error) && object.content
|
|
336
|
+
? parseReleaseKindContent(object.content)
|
|
337
|
+
: null;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
release,
|
|
341
|
+
...(wantsCover ? { cover: cover ?? null } : {}),
|
|
342
|
+
...(wantsCredits ? { credits: credits ?? [] } : {}),
|
|
343
|
+
...(wantsKind ? { kind: kind ?? null } : {}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Release ──────────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* A release with its cover, credits, and resolved tracklist.
|
|
351
|
+
*
|
|
352
|
+
* Identity (the release object) is HARD — a failure here is a failed read.
|
|
353
|
+
* Decoration (cover, credits) is SOFT: a release with no cover extension set is a
|
|
354
|
+
* normal state, not a broken page, so those reads swallow their errors. That
|
|
355
|
+
* split keeps a half-configured
|
|
356
|
+
* release renderable.
|
|
357
|
+
*/
|
|
358
|
+
export type ReleaseInclude = "trackCredits";
|
|
359
|
+
|
|
360
|
+
export interface GetReleaseOptions {
|
|
361
|
+
include?: readonly ReleaseInclude[];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function getReleaseDetail(
|
|
365
|
+
client: MisoClient,
|
|
366
|
+
releaseId: string,
|
|
367
|
+
options: GetReleaseOptions = {},
|
|
368
|
+
): Promise<ReleaseDetail> {
|
|
369
|
+
const { release, cover, credits, kind } = await getReleaseResources(
|
|
370
|
+
client,
|
|
371
|
+
releaseId,
|
|
372
|
+
["cover", "credits", "kind"],
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const recordingIds = release.tracks.map((track) => track.recordingId);
|
|
376
|
+
const mixPackageId = client.config.protocol.releaseMixReference;
|
|
377
|
+
const [titles, masterReferences, mixReferences, trackCredits] = await Promise.all([
|
|
378
|
+
getRecordingTitles(
|
|
379
|
+
client.protocol,
|
|
380
|
+
client.graphql,
|
|
381
|
+
recordingIds,
|
|
382
|
+
client.config.deployment.miso,
|
|
383
|
+
),
|
|
384
|
+
getRecordingMasterReferencesByIds(
|
|
385
|
+
client.protocol,
|
|
386
|
+
recordingIds,
|
|
387
|
+
client.config.protocol.recordingMasterReference,
|
|
388
|
+
).catch(() => ({})),
|
|
389
|
+
mixPackageId
|
|
390
|
+
? getReleaseMixReferences(client.protocol, releaseId, mixPackageId).catch(() => null)
|
|
391
|
+
: Promise.resolve(null),
|
|
392
|
+
options.include?.includes("trackCredits")
|
|
393
|
+
? getTrackCreditsForRecordingIds(client, recordingIds)
|
|
394
|
+
: Promise.resolve(undefined),
|
|
395
|
+
]);
|
|
396
|
+
const masterBlobIds: Record<string, string> = {};
|
|
397
|
+
for (const [recordingId, blobId] of Object.entries(masterReferences)) {
|
|
398
|
+
if (blobId) masterBlobIds[recordingId] = u256ToB64Url(blobId);
|
|
399
|
+
}
|
|
400
|
+
const mixBlobIds: Record<number, string> = {};
|
|
401
|
+
for (let index = 0; index < (mixReferences?.length ?? 0); index += 1) {
|
|
402
|
+
const reference = mixReferences?.[index];
|
|
403
|
+
if (reference) mixBlobIds[index] = walrusBlobIdFromU256(reference.blobId);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const creditViews = credits ?? [];
|
|
407
|
+
return {
|
|
408
|
+
id: release.id,
|
|
409
|
+
title: release.title,
|
|
410
|
+
subtitle: null,
|
|
411
|
+
kind: kind ?? null,
|
|
412
|
+
state: toWorkState(release.state),
|
|
413
|
+
publishedAtMs:
|
|
414
|
+
release.state.type === "Published" ? release.state.timestampMs : null,
|
|
415
|
+
cover: cover ?? null,
|
|
416
|
+
credits: creditViews,
|
|
417
|
+
primaryArtists: primaryArtistNames(creditViews),
|
|
418
|
+
discCount: release.tracks.length > 0 ? 1 : 0,
|
|
419
|
+
tracks: toTracks(release, titles, masterBlobIds, mixBlobIds),
|
|
420
|
+
...(trackCredits !== undefined ? { trackCredits } : {}),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function getTrackCreditsForRecordingIds(
|
|
425
|
+
client: MisoClient,
|
|
426
|
+
recordingIds: readonly string[],
|
|
427
|
+
): Promise<Record<string, TrackCredits>> {
|
|
428
|
+
const { compositionCredits, recordingCredits } = client.config.protocol;
|
|
429
|
+
const tracks = await getTrackCreditsByRecordingIds(
|
|
430
|
+
client.protocol,
|
|
431
|
+
client.graphql,
|
|
432
|
+
recordingIds,
|
|
433
|
+
{
|
|
434
|
+
misoPackageId: client.config.deployment.miso,
|
|
435
|
+
compositionCreditsPackageId: compositionCredits,
|
|
436
|
+
recordingCreditsPackageId: recordingCredits,
|
|
437
|
+
},
|
|
438
|
+
);
|
|
439
|
+
return Object.fromEntries(
|
|
440
|
+
Object.entries(tracks).map(([id, track]) => [
|
|
441
|
+
id,
|
|
442
|
+
{
|
|
443
|
+
compositionCredits: toCredits(track.compositionCredits),
|
|
444
|
+
recordingCredits: {
|
|
445
|
+
credits: toCredits(track.recordingCredits.credits),
|
|
446
|
+
primaryArtistIds: [...track.recordingCredits.primaryArtistIds],
|
|
447
|
+
featuredArtistIds: [...track.recordingCredits.featuredArtistIds],
|
|
448
|
+
},
|
|
449
|
+
} satisfies TrackCredits,
|
|
450
|
+
]),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Per-track credits for a release, keyed by recording id. A track with no credits
|
|
456
|
+
* set maps to an empty entry rather than being absent, so a caller can tell
|
|
457
|
+
* "nothing credited" from "no such track".
|
|
458
|
+
*/
|
|
459
|
+
export async function getTrackCredits(
|
|
460
|
+
client: MisoClient,
|
|
461
|
+
releaseId: string,
|
|
462
|
+
): Promise<Record<string, TrackCredits>> {
|
|
463
|
+
const release = await getReleaseById(client.protocol, releaseId);
|
|
464
|
+
const recordingIds = release.tracks.map((track) => track.recordingId);
|
|
465
|
+
return getTrackCreditsForRecordingIds(client, recordingIds);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Pressing ─────────────────────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
/** Everything a Pressing page renders. `null` when no such Pressing exists. */
|
|
471
|
+
export async function getPressingDetail(
|
|
472
|
+
client: MisoClient,
|
|
473
|
+
pressingId: string,
|
|
474
|
+
options: GetReleaseOptions = {},
|
|
475
|
+
): Promise<PressingDetail | null> {
|
|
476
|
+
const pressing = await getPressingView(client, pressingId);
|
|
477
|
+
if (!pressing) return null;
|
|
478
|
+
const release = await getReleaseDetail(client, pressing.releaseId, options);
|
|
479
|
+
return { pressing, release };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* A Pressing page plus one currency-specific Listing. Unlike `getSaleDetail`,
|
|
484
|
+
* this starts from a Pressing id, which is the durable route and Party-feature
|
|
485
|
+
* reference exposed to users.
|
|
486
|
+
*/
|
|
487
|
+
export async function getPressingSaleDetail(
|
|
488
|
+
client: MisoClient,
|
|
489
|
+
pressingId: string,
|
|
490
|
+
currencyType: string,
|
|
491
|
+
options: GetReleaseOptions = {},
|
|
492
|
+
): Promise<SaleDetail | null> {
|
|
493
|
+
const pressing = await getPressingView(client, pressingId);
|
|
494
|
+
if (!pressing) return null;
|
|
495
|
+
|
|
496
|
+
const [listing, release] = await Promise.all([
|
|
497
|
+
getListingView(client, pressing.id, currencyType),
|
|
498
|
+
getReleaseDetail(client, pressing.releaseId, options),
|
|
499
|
+
]);
|
|
500
|
+
if (!listing) return null;
|
|
501
|
+
return { sale: { pressing, listing }, release };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The confirmation preview behind "paste a pressing id to pin it". Verifies the
|
|
506
|
+
* object really is a `Pressing` before spending reads on it — a release id or a
|
|
507
|
+
* record id pasted by mistake must come back as `null`, not as a half-built card.
|
|
508
|
+
*/
|
|
509
|
+
export async function getPressingPreview(
|
|
510
|
+
client: MisoClient,
|
|
511
|
+
pressingId: string,
|
|
512
|
+
): Promise<PressingPreview | null> {
|
|
513
|
+
const pressing = await getPressingView(client, pressingId);
|
|
514
|
+
if (!pressing) return null;
|
|
515
|
+
|
|
516
|
+
const { release, cover } = await getReleaseResources(
|
|
517
|
+
client,
|
|
518
|
+
pressing.releaseId,
|
|
519
|
+
["cover"],
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
pressingId,
|
|
524
|
+
title: release.title,
|
|
525
|
+
subtitle: null,
|
|
526
|
+
coverUrl: cover?.still.url ?? null,
|
|
527
|
+
edition: pressing.edition,
|
|
528
|
+
supply: pressing.supply,
|
|
529
|
+
maxSupply: pressing.maxSupply,
|
|
530
|
+
trackCount: release.tracks.length,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Everything a currency-specific buy page renders. The Listing is derived from
|
|
536
|
+
* the release's Pressing and the requested currency, never found through mutable
|
|
537
|
+
* lookup state.
|
|
538
|
+
*/
|
|
539
|
+
export async function getSaleDetail(
|
|
540
|
+
client: MisoClient,
|
|
541
|
+
releaseId: string,
|
|
542
|
+
edition: number,
|
|
543
|
+
currencyType: string,
|
|
544
|
+
options: GetReleaseOptions = {},
|
|
545
|
+
): Promise<SaleDetail | null> {
|
|
546
|
+
const sales = requireRecordSalesDeployment(client.config.recordSales);
|
|
547
|
+
const sale = await getSale(client.protocol, {
|
|
548
|
+
releaseId,
|
|
549
|
+
edition,
|
|
550
|
+
currencyType,
|
|
551
|
+
recordPackageId: sales.recordPackageId,
|
|
552
|
+
recordShopPackageId: sales.recordShopPackageId,
|
|
553
|
+
});
|
|
554
|
+
if (!sale.pressing || !sale.listing) return null;
|
|
555
|
+
const release = await getReleaseDetail(client, sale.pressing.releaseId, options);
|
|
556
|
+
return { sale: toSaleView(sale.pressing, sale.listing), release };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ── Discover ─────────────────────────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The records currently on sale.
|
|
563
|
+
*
|
|
564
|
+
* Configured by release + edition + currency: both addresses are deterministic.
|
|
565
|
+
*/
|
|
566
|
+
export async function getDiscoverShelf(
|
|
567
|
+
client: MisoClient,
|
|
568
|
+
): Promise<DiscoverItem[]> {
|
|
569
|
+
const configuredSales = [...client.config.discoverSales];
|
|
570
|
+
const sales = requireRecordSalesDeployment(client.config.recordSales);
|
|
571
|
+
const settled = await Promise.all(
|
|
572
|
+
configuredSales.map(async (configured) => ({
|
|
573
|
+
configured,
|
|
574
|
+
result: await getSale(client.protocol, {
|
|
575
|
+
...configured,
|
|
576
|
+
recordPackageId: sales.recordPackageId,
|
|
577
|
+
recordShopPackageId: sales.recordShopPackageId,
|
|
578
|
+
}),
|
|
579
|
+
})),
|
|
580
|
+
);
|
|
581
|
+
const available = settled.filter(
|
|
582
|
+
(item): item is typeof item & {
|
|
583
|
+
result: { pressing: ContractPressingView; listing: ContractListingView };
|
|
584
|
+
} => item.result.pressing !== null && item.result.listing !== null,
|
|
585
|
+
);
|
|
586
|
+
const releaseIds = [...new Set(available.map((item) => item.result.pressing.releaseId))];
|
|
587
|
+
if (releaseIds.length === 0) return [];
|
|
588
|
+
|
|
589
|
+
const [releases, coverViews, credits] = await Promise.all([
|
|
590
|
+
getReleasesByIds(client.protocol, releaseIds),
|
|
591
|
+
getReleaseCoversByIds(client.protocol, releaseIds, client.config.protocol.releaseCoverArt).catch(() => ({}) as Partial<Record<string, ReleaseCoverView>>),
|
|
592
|
+
getReleaseCreditsByIds(
|
|
593
|
+
client.protocol,
|
|
594
|
+
releaseIds,
|
|
595
|
+
client.config.protocol.releaseCredits,
|
|
596
|
+
).catch(() => ({}) as Partial<Record<string, CreditView[]>>),
|
|
597
|
+
]);
|
|
598
|
+
|
|
599
|
+
return available.flatMap(({ result }) => {
|
|
600
|
+
const releaseId = result.pressing.releaseId;
|
|
601
|
+
const release = releases[releaseId];
|
|
602
|
+
if (!release) return [];
|
|
603
|
+
const cover = toCover(
|
|
604
|
+
client.config.walrusAggregatorUrl,
|
|
605
|
+
coverViews[releaseId] ?? null,
|
|
606
|
+
);
|
|
607
|
+
return [
|
|
608
|
+
{
|
|
609
|
+
sale: toSaleView(result.pressing, result.listing),
|
|
610
|
+
releaseId,
|
|
611
|
+
title: release.title,
|
|
612
|
+
artist: primaryArtistNames(toCredits(credits[releaseId] ?? null)).join(
|
|
613
|
+
", ",
|
|
614
|
+
),
|
|
615
|
+
coverUrl: cover?.still.url ?? null,
|
|
616
|
+
},
|
|
617
|
+
];
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ── Record → release ─────────────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* A record's parent release. Fresh `Record` objects expose `release_id`.
|
|
625
|
+
*
|
|
626
|
+
* This answer is IMMUTABLE for the life of the record, which is what lets the
|
|
627
|
+
* endpoint in front of it cache for a year.
|
|
628
|
+
*/
|
|
629
|
+
export type RecordAlbumInclude = "release" | "trackCredits";
|
|
630
|
+
|
|
631
|
+
export interface GetRecordAlbumOptions {
|
|
632
|
+
include?: readonly RecordAlbumInclude[];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export async function getRecordAlbum(
|
|
636
|
+
client: MisoClient,
|
|
637
|
+
recordId: string,
|
|
638
|
+
options: GetRecordAlbumOptions = {},
|
|
639
|
+
): Promise<RecordAlbum | null> {
|
|
640
|
+
try {
|
|
641
|
+
const sales = requireRecordSalesDeployment(client.config.recordSales);
|
|
642
|
+
const record = await getRecord(client.protocol, recordId, sales.recordPackageId);
|
|
643
|
+
if (!record) return null;
|
|
644
|
+
const releaseId = record.releaseId;
|
|
645
|
+
const includeRelease =
|
|
646
|
+
options.include?.some(
|
|
647
|
+
(part) => part === "release" || part === "trackCredits",
|
|
648
|
+
) ?? false;
|
|
649
|
+
const release =
|
|
650
|
+
includeRelease && releaseId
|
|
651
|
+
? await getReleaseDetail(client, releaseId, {
|
|
652
|
+
include: options.include?.includes("trackCredits")
|
|
653
|
+
? ["trackCredits"]
|
|
654
|
+
: [],
|
|
655
|
+
})
|
|
656
|
+
: includeRelease
|
|
657
|
+
? null
|
|
658
|
+
: undefined;
|
|
659
|
+
return {
|
|
660
|
+
recordId,
|
|
661
|
+
releaseId,
|
|
662
|
+
...(includeRelease ? { release } : {}),
|
|
663
|
+
};
|
|
664
|
+
} catch (e) {
|
|
665
|
+
if (isNotFound(e)) return null;
|
|
666
|
+
throw e;
|
|
667
|
+
}
|
|
668
|
+
}
|