@feralfile/cli 1.5.0 → 1.6.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/dist/package.json
CHANGED
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.findCommand = void 0;
|
|
40
|
+
exports.buildRasterMediaItems = buildRasterMediaItems;
|
|
40
41
|
exports.parseLimitOption = parseLimitOption;
|
|
41
42
|
exports.decideActions = decideActions;
|
|
42
43
|
const commander_1 = require("commander");
|
|
@@ -68,7 +69,7 @@ const DP1_MAX_ITEMS = 1024;
|
|
|
68
69
|
const RASTER_PAGE_SIZE = 100;
|
|
69
70
|
exports.findCommand = new commander_1.Command('find')
|
|
70
71
|
.description('Find an artwork on the web and build a DP-1 playlist')
|
|
71
|
-
.argument('<input>', 'URL (Objkt / fxhash / Art Blocks / OpenSea / SuperRare / Feral File / Neort / Verse), `ethereum:{contract}:{tokenId}`, `tezos:{contract}:{tokenId}`, or a wallet address')
|
|
72
|
+
.argument('<input>', 'URL (Objkt / fxhash / Art Blocks / OpenSea / SuperRare / Feral File / Neort / Verse / Raster), `ethereum:{contract}:{tokenId}`, `tezos:{contract}:{tokenId}`, or a wallet address')
|
|
72
73
|
.option('-o, --output <path>', 'Save the playlist to this file (default: ./<slug>.json)')
|
|
73
74
|
.option('-l, --limit <n>', 'Max tokens to include from the series (default: all)')
|
|
74
75
|
.option('-p, --play', 'Play the playlist on an FF1 device after building')
|
|
@@ -83,7 +84,7 @@ exports.findCommand = new commander_1.Command('find')
|
|
|
83
84
|
const parsed = (0, marketplace_url_1.parseFindInput)(input);
|
|
84
85
|
if (!parsed) {
|
|
85
86
|
console.error(chalk_1.default.red('Could not understand input.'));
|
|
86
|
-
console.error(chalk_1.default.dim('Supported URLs: Objkt, fxhash, Art Blocks, OpenSea, SuperRare, Feral File, Neort, Verse. ' +
|
|
87
|
+
console.error(chalk_1.default.dim('Supported URLs: Objkt, fxhash, Art Blocks, OpenSea, SuperRare, Feral File, Neort, Verse, Raster. ' +
|
|
87
88
|
'Or: `ethereum:{contract}:{tokenId}`, `tezos:{contract}:{tokenId}`, ' +
|
|
88
89
|
'or a wallet address (`0x...` / `tz1.../tz2.../tz3...`).'));
|
|
89
90
|
process.exit(1);
|
|
@@ -125,7 +126,7 @@ exports.findCommand = new commander_1.Command('find')
|
|
|
125
126
|
seriesHasMore = fetched.hasMore;
|
|
126
127
|
}
|
|
127
128
|
else {
|
|
128
|
-
tokens = [target.coords];
|
|
129
|
+
tokens = [{ ...target.coords, mediaUrl: null, mediaType: null }];
|
|
129
130
|
}
|
|
130
131
|
if (tokens.length === 0) {
|
|
131
132
|
console.error(chalk_1.default.red('Series has no tokens on supported chains (Ethereum + Tezos mainnet).'));
|
|
@@ -148,11 +149,25 @@ exports.findCommand = new commander_1.Command('find')
|
|
|
148
149
|
// display seconds), not concurrency — concurrency is hardcoded inside.
|
|
149
150
|
// Omit it for auto timing: video/audio items carry no duration and play
|
|
150
151
|
// their natural length (DP-1 §4.1); static items get the config default.
|
|
151
|
-
|
|
152
|
+
let items = await getNFTTokenInfoBatch(tokens.map((t) => ({
|
|
152
153
|
chain: t.chain,
|
|
153
154
|
contractAddress: t.contract,
|
|
154
155
|
tokenId: t.tokenId,
|
|
155
156
|
})));
|
|
157
|
+
// FF-indexer bypass for Raster-minted tokens. The indexer doesn't carry
|
|
158
|
+
// Raster's tokens, so it returns nothing — but Raster handed us each
|
|
159
|
+
// token's playable `media.contentUrl`. Build DP-1 items straight from
|
|
160
|
+
// that (off-chain provenance, same shape as the Neort path), so the
|
|
161
|
+
// series still builds into a signable, verifiable playlist instead of
|
|
162
|
+
// failing the way it did before. The indexer is tried first so tokens
|
|
163
|
+
// it *can* resolve keep their richer on-chain provenance.
|
|
164
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
165
|
+
const rasterItems = buildRasterMediaItems(tokens, target);
|
|
166
|
+
if (rasterItems.length > 0) {
|
|
167
|
+
console.log(chalk_1.default.dim(` FF indexer returned nothing; building ${rasterItems.length} item${rasterItems.length === 1 ? '' : 's'} directly from Raster media.`));
|
|
168
|
+
items = rasterItems;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
156
171
|
if (!Array.isArray(items) || items.length === 0) {
|
|
157
172
|
console.error(chalk_1.default.red('No tokens could be indexed; cannot build playlist.'));
|
|
158
173
|
console.error(chalk_1.default.dim(' The FF indexer may be under load when warming a series of previously-unseen tokens.'));
|
|
@@ -217,6 +232,14 @@ async function resolveTarget(parsed, skipPrompt) {
|
|
|
217
232
|
const coords = await (0, verse_marketplace_1.resolveVerseSeries)(parsed.slug);
|
|
218
233
|
return resolveCoords(coords);
|
|
219
234
|
}
|
|
235
|
+
if (parsed.kind === 'raster-artwork') {
|
|
236
|
+
const summary = await (0, raster_client_1.resolveSlugToArtwork)(parsed.slug);
|
|
237
|
+
if (summary === null) {
|
|
238
|
+
throw new Error(`No artwork found on Raster for slug "${parsed.slug}". ` +
|
|
239
|
+
'Check the URL — Raster artwork URLs are raster.art/artwork/{slug}.');
|
|
240
|
+
}
|
|
241
|
+
return { kind: 'series', summary };
|
|
242
|
+
}
|
|
220
243
|
if (parsed.kind === 'address') {
|
|
221
244
|
const artist = await (0, raster_client_1.resolveAddressToArtist)(parsed.address);
|
|
222
245
|
if (artist === null) {
|
|
@@ -259,6 +282,32 @@ function playlistTitleFor(target, items) {
|
|
|
259
282
|
}
|
|
260
283
|
return items[0]?.title ?? `Token ${target.coords.tokenId}`;
|
|
261
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Build DP-1 items directly from Raster's per-token media, bypassing the FF
|
|
287
|
+
* indexer. Used as a fallback when the indexer resolves nothing — tokens
|
|
288
|
+
* without a Raster `mediaUrl` are dropped (nothing playable to point at).
|
|
289
|
+
*
|
|
290
|
+
* Item shape matches the Neort off-chain path (`buildUrlItem` →
|
|
291
|
+
* `provenance.offChainURI`), so the resulting playlist signs and verifies
|
|
292
|
+
* normally. Titles are "<Series> #<tokenId>" for series, falling back to the
|
|
293
|
+
* series/coords title for a single token.
|
|
294
|
+
*/
|
|
295
|
+
function buildRasterMediaItems(tokens, target) {
|
|
296
|
+
const seriesTitle = target.kind === 'series' ? target.summary.title : undefined;
|
|
297
|
+
const items = [];
|
|
298
|
+
for (const t of tokens) {
|
|
299
|
+
if (!t.mediaUrl) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const title = seriesTitle ? `${seriesTitle} #${t.tokenId}` : `Token ${t.tokenId}`;
|
|
303
|
+
// No explicit duration: auto timing (video/audio play their natural
|
|
304
|
+
// length). `mediaType` carries Raster's type so an extensionless IPFS
|
|
305
|
+
// contentUrl is still classified correctly — otherwise a video would be
|
|
306
|
+
// stamped a fixed duration and play as a still.
|
|
307
|
+
items.push(buildUrlItem(t.mediaUrl, undefined, { title, mimeType: t.mediaType ?? undefined }));
|
|
308
|
+
}
|
|
309
|
+
return items;
|
|
310
|
+
}
|
|
262
311
|
function parseLimitOption(limitStr) {
|
|
263
312
|
if (limitStr === undefined) {
|
|
264
313
|
return Number.POSITIVE_INFINITY;
|
|
@@ -300,6 +349,8 @@ async function fetchTokens(summary, limit) {
|
|
|
300
349
|
chain: t.chain,
|
|
301
350
|
contract: t.contractAddress,
|
|
302
351
|
tokenId: t.tokenId,
|
|
352
|
+
mediaUrl: t.mediaUrl,
|
|
353
|
+
mediaType: t.mediaType,
|
|
303
354
|
});
|
|
304
355
|
}
|
|
305
356
|
if (hasMore || page.nextCursor === null) {
|
|
@@ -26,6 +26,7 @@ exports.parseOpenSea = parseOpenSea;
|
|
|
26
26
|
exports.parseSuperRare = parseSuperRare;
|
|
27
27
|
exports.parseNeort = parseNeort;
|
|
28
28
|
exports.parseVerse = parseVerse;
|
|
29
|
+
exports.parseRaster = parseRaster;
|
|
29
30
|
exports.parseFeralFile = parseFeralFile;
|
|
30
31
|
const ETH_ADDR = /^0x[a-fA-F0-9]{40}$/;
|
|
31
32
|
const TEZOS_ADDR = /^(tz1|tz2|tz3)[1-9A-HJ-NP-Za-km-z]{33}$/;
|
|
@@ -101,6 +102,9 @@ function parseMarketplaceUrl(url) {
|
|
|
101
102
|
if (host === 'verse.works' || host.endsWith('.verse.works')) {
|
|
102
103
|
return parseVerse(url);
|
|
103
104
|
}
|
|
105
|
+
if (host === 'raster.art' || host.endsWith('.raster.art')) {
|
|
106
|
+
return parseRaster(url);
|
|
107
|
+
}
|
|
104
108
|
return null;
|
|
105
109
|
}
|
|
106
110
|
/**
|
|
@@ -382,6 +386,27 @@ function parseVerse(url) {
|
|
|
382
386
|
'/items/ethereum/{contract}/{tokenId} or /series/{slug}.',
|
|
383
387
|
};
|
|
384
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Raster URL forms (raster.art):
|
|
391
|
+
* raster.art/artwork/{slug} — artwork (series) detail page
|
|
392
|
+
*
|
|
393
|
+
* Unlike the other marketplaces, Raster is already the find flow's backend
|
|
394
|
+
* resolver — so a Raster URL resolves to an artwork by slug via
|
|
395
|
+
* `artworkBySlug` and (crucially) builds DP-1 items straight from Raster's
|
|
396
|
+
* own `media.contentUrl`, bypassing the FF indexer the way Neort does. This
|
|
397
|
+
* is what lets Raster-minted tokens (which the FF indexer doesn't carry)
|
|
398
|
+
* build a verifiable playlist without `--skip-verify`.
|
|
399
|
+
*/
|
|
400
|
+
function parseRaster(url) {
|
|
401
|
+
const m = /^\/artwork\/([A-Za-z0-9][A-Za-z0-9_-]*)\/?$/.exec(url.pathname);
|
|
402
|
+
if (m) {
|
|
403
|
+
return { kind: 'raster-artwork', slug: m[1] };
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
kind: 'unsupported',
|
|
407
|
+
reason: `Raster URL not recognized: ${url.pathname}. Expected /artwork/{slug}.`,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
385
410
|
/**
|
|
386
411
|
* Feral File URL forms (source of truth: feralfile-client app-routing
|
|
387
412
|
* + collection-routing modules):
|
|
@@ -536,6 +536,10 @@ function detectMimeType(url) {
|
|
|
536
536
|
* (video/audio URLs play their natural length per DP-1 §4.1)
|
|
537
537
|
* @param {Object} [options] - Optional configuration
|
|
538
538
|
* @param {string} [options.title] - Optional item title override
|
|
539
|
+
* @param {string} [options.mimeType] - Source media type hint for timing
|
|
540
|
+
* (DP-1 §4.1). Needed when the URL has no file extension to detect from —
|
|
541
|
+
* e.g. extensionless IPFS gateway URLs, where without this hint a video
|
|
542
|
+
* would be misclassified as a static image and stamped a fixed duration.
|
|
539
543
|
* @returns {Object} DP1 playlist item
|
|
540
544
|
*/
|
|
541
545
|
function buildUrlItem(url, duration, options = {}) {
|
|
@@ -580,7 +584,7 @@ function buildUrlItem(url, duration, options = {}) {
|
|
|
580
584
|
margin: 0,
|
|
581
585
|
},
|
|
582
586
|
};
|
|
583
|
-
return applyItemTiming(item, { sourceUrl }, duration);
|
|
587
|
+
return applyItemTiming(item, { sourceUrl, mimeType: options.mimeType }, duration);
|
|
584
588
|
}
|
|
585
589
|
module.exports = {
|
|
586
590
|
convertTokenToDP1Item,
|
|
@@ -58,6 +58,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
58
58
|
})();
|
|
59
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
60
|
exports.resolveTokenToArtwork = resolveTokenToArtwork;
|
|
61
|
+
exports.resolveSlugToArtwork = resolveSlugToArtwork;
|
|
61
62
|
exports.resolveAddressToArtist = resolveAddressToArtist;
|
|
62
63
|
exports.listArtworkTokens = listArtworkTokens;
|
|
63
64
|
exports.listArtistArtworks = listArtistArtworks;
|
|
@@ -150,6 +151,30 @@ async function resolveTokenToArtwork(chain, contract, tokenId) {
|
|
|
150
151
|
artists: (artwork.artists ?? []).map(toRasterArtist),
|
|
151
152
|
};
|
|
152
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve a `raster.art/artwork/{slug}` URL to its artwork (series) summary.
|
|
156
|
+
*
|
|
157
|
+
* One round trip via `artworkBySlug`. Returns `null` when the slug is
|
|
158
|
+
* unknown (Raster models not-found as a null query field, not an HTTP 404).
|
|
159
|
+
*/
|
|
160
|
+
async function resolveSlugToArtwork(slug) {
|
|
161
|
+
const data = await rasterQuery(`query ResolveSlug($slug: String!) {
|
|
162
|
+
artworkBySlug(slug: $slug) {
|
|
163
|
+
id
|
|
164
|
+
title
|
|
165
|
+
artists { id name slug }
|
|
166
|
+
}
|
|
167
|
+
}`, { slug });
|
|
168
|
+
const artwork = data.artworkBySlug;
|
|
169
|
+
if (!artwork) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
artworkId: artwork.id,
|
|
174
|
+
title: artwork.title,
|
|
175
|
+
artists: (artwork.artists ?? []).map(toRasterArtist),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
153
178
|
/**
|
|
154
179
|
* Resolve a wallet address to the Raster artist who claims it.
|
|
155
180
|
*
|
|
@@ -180,7 +205,7 @@ async function listArtworkTokens(artworkId, options = {}) {
|
|
|
180
205
|
const data = await rasterQuery(`query ArtworkTokens($id: ID!, $first: Int, $after: String) {
|
|
181
206
|
artwork(id: $id) {
|
|
182
207
|
tokens(first: $first, after: $after) {
|
|
183
|
-
nodes { chainId contractAddress tokenId }
|
|
208
|
+
nodes { chainId contractAddress tokenId media { contentUrl contentType previewType } }
|
|
184
209
|
pageInfo { hasNextPage endCursor }
|
|
185
210
|
}
|
|
186
211
|
}
|
|
@@ -200,6 +225,10 @@ async function listArtworkTokens(artworkId, options = {}) {
|
|
|
200
225
|
chain,
|
|
201
226
|
contractAddress: raw.contractAddress,
|
|
202
227
|
tokenId: raw.tokenId,
|
|
228
|
+
mediaUrl: raw.media?.contentUrl ?? null,
|
|
229
|
+
// Prefer a real MIME (`contentType`) when Raster populates it; today it
|
|
230
|
+
// is usually empty, so fall back to the category-prefixed `previewType`.
|
|
231
|
+
mediaType: raw.media?.contentType || raw.media?.previewType || null,
|
|
203
232
|
});
|
|
204
233
|
}
|
|
205
234
|
const { hasNextPage, endCursor } = data.artwork.tokens.pageInfo;
|