playlist-data-engine 1.5.8 → 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.
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});var m=function(a,t){return m=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,r){e.__proto__=r}||function(e,r){for(var s in r)r.hasOwnProperty(s)&&(e[s]=r[s])},m(a,t)};function p(a,t){m(a,t);function e(){this.constructor=a}a.prototype=t===null?Object.create(t):(e.prototype=t.prototype,new e)}function c(a,t,e,r){return new(e||(e=Promise))(function(s,n){function o(i){try{u(r.next(i))}catch(f){n(f)}}function l(i){try{u(r.throw(i))}catch(f){n(f)}}function u(i){i.done?s(i.value):new e(function(f){f(i.value)}).then(o,l)}u((r=r.apply(a,[])).next())})}function h(a,t){var e={label:0,sent:function(){if(n[0]&1)throw n[1];return n[1]},trys:[],ops:[]},r,s,n,o;return o={next:l(0),throw:l(1),return:l(2)},typeof Symbol=="function"&&(o[Symbol.iterator]=function(){return this}),o;function l(i){return function(f){return u([i,f])}}function u(i){if(r)throw new TypeError("Generator is already executing.");for(;e;)try{if(r=1,s&&(n=i[0]&2?s.return:i[0]?s.throw||((n=s.return)&&n.call(s),0):s.next)&&!(n=n.call(s,i[1])).done)return n;switch(s=0,n&&(i=[i[0]&2,n.value]),i[0]){case 0:case 1:n=i;break;case 4:return e.label++,{value:i[1],done:!1};case 5:e.label++,s=i[1],i=[0];continue;case 7:i=e.ops.pop(),e.trys.pop();continue;default:if(n=e.trys,!(n=n.length>0&&n[n.length-1])&&(i[0]===6||i[0]===2)){e=0;continue}if(i[0]===3&&(!n||i[1]>n[0]&&i[1]<n[3])){e.label=i[1];break}if(i[0]===6&&e.label<n[1]){e.label=n[1],n=i;break}if(n&&e.label<n[2]){e.label=n[2],e.ops.push(i);break}n[2]&&e.ops.pop(),e.trys.pop();continue}i=t.call(a,e)}catch(f){i=[6,f],s=0}finally{r=n=0}if(i[0]&5)throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}}var y=(function(){function a(t,e,r){if(e===void 0&&(e="musicnn"),r===void 0&&(r=!1),this.essentia=null,this.module=null,this.frameSize=512,this.sampleRate=16e3,this.extractorType=e,this.extractorType==="musicnn")this.frameSize=512;else if(this.extractorType==="vggish")this.frameSize=400;else if(this.extractorType==="tempocnn")this.frameSize=1024;else throw Error("Invalid 'extractorType' choice! Available types are [musicnn', 'vggish', 'tempocnn']");this.essentia=new t.EssentiaJS(r),this.module=t}return a.prototype.arrayToVector=function(t){return this.module.arrayToVector(t)},a.prototype.vectorToArray=function(t){return this.module.vectorToArray(t)},a.prototype.getAudioBufferFromURL=function(t,e){return c(this,void 0,void 0,function(){var r,s,n;return h(this,function(o){switch(o.label){case 0:return[4,fetch(t)];case 1:return r=o.sent(),[4,r.arrayBuffer()];case 2:return s=o.sent(),[4,e.decodeAudioData(s)];case 3:return n=o.sent(),[2,n]}})})},a.prototype.audioBufferToMonoSignal=function(t){if(t.numberOfChannels===1)return t.getChannelData(0);if(t.numberOfChannels===2){var e=this.arrayToVector(t.getChannelData(0)),r=this.arrayToVector(t.getChannelData(1)),s=this.essentia.MonoMixer(e,r).audio;return this.vectorToArray(s)}throw new Error("Unexpected number of channels found in audio buffer. Only accepts mono or stereo audio buffers.")},a.prototype.downsampleAudioBuffer=function(t){var e=new OfflineAudioContext(1,t.duration*this.sampleRate,this.sampleRate),r=e.createBuffer(1,t.length,t.sampleRate);r.copyToChannel(this.audioBufferToMonoSignal(t),0);var s=e.createBufferSource();return s.buffer=r,s.connect(e.destination),new Promise(function(n){e.oncomplete=function(o){var l=o.renderedBuffer,u=l.getChannelData(0);n(u)},e.startRendering(),s.start(0)})},a.prototype.compute=function(t){var e;switch(t instanceof Float32Array?e=this.arrayToVector(t):e=t,this.extractorType){case"musicnn":{if(t.length!=this.frameSize)throw new Error("The chosen `extractorType` only works with an audio signal frame size of "+this.frameSize);var r=this.essentia.TensorflowInputMusiCNN(e);return{melSpectrum:this.vectorToArray(r.bands),frameSize:1,patchSize:187,melBandsSize:96}}case"vggish":{if(t.length!=this.frameSize)throw new Error("The chosen `extractorType` only works with an audio signal frame size of 400 "+this.frameSize);var r=this.essentia.TensorflowInputVGGish(e);return{melSpectrum:this.vectorToArray(r.bands),frameSize:1,patchSize:96,melBandsSize:64}}case"tempocnn":{if(t.length!=this.frameSize)throw Error("The chosen `extractorType` only works with an audio signal frame size of "+this.frameSize);var r=this.essentia.TensorflowInputTempoCNN(e);return{melSpectrum:this.vectorToArray(r.bands),frameSize:1,patchSize:256,melBandsSize:40}}default:throw Error("Invalid 'extractorType' choice! Available types are [musicnn', 'vggish', 'tempocnn']")}},a.prototype.computeFrameWise=function(t,e){var r;e?r=e:r=this.frameSize;for(var s=this.essentia.FrameGenerator(t,this.frameSize,r),n=[],o=null,l=0;l<s.size();l++)o=this.compute(this.vectorToArray(s.get(l))),n.push(o.melSpectrum);return o.melSpectrum=n,o.frameSize=s.size(),s.delete(),o},a.prototype.delete=function(){this.essentia.delete()},a.prototype.shutdown=function(){this.essentia.shutdown()},a})();var d=(function(){function a(t,e,r){this.model=null,this.audioSampleRate=16e3,this.tf=null,this.isReady=!1,this.modelPath="",this.IS_TRAIN=null,this.randomTensorInput=null,this.minimumInputFrameSize=null,this.tf=t,this.IS_TRAIN=this.tf.tensor([0],[1],"bool"),this.modelPath=e,this.isReady=!!this.model}return a.prototype.initialize=function(){return c(this,void 0,void 0,function(){var t;return h(this,function(e){switch(e.label){case 0:return t=this,[4,this.tf.loadGraphModel(this.modelPath)];case 1:return t.model=e.sent(),this.isReady=!0,[2]}})})},a.prototype.arrayToTensorAsBatches=function(t,e,r,s){s===void 0&&(s=!1);var n=this.tf.tensor(t,e,"float32"),o,l;return s?e[0]===r?n.as3D(1,r,e[1]):e[0]>r?e[0]%r!=0?(l=Math.floor(e[0]/r)+1,o=this.tf.zeros([Math.floor(l*r-t.length),e[1]],"float32"),n=n.concat(o),o.dispose(),n.as3D(l,r,e[1])):(l=Math.floor(e[0]/r),o=this.tf.zeros([Math.floor(l*r-t.length),e[1]],"float32"),n=n.concat(o),o.dispose(),n.as3D(l,r,e[1])):(l=1,o=this.tf.zeros([r-e[0],e[1]]),n=n.concat(o),o.dispose(),n.as3D(l,r,e[1])):(this.assertMinimumFeatureInputSize({melSpectrum:t,frameSize:e[0],melBandsSize:e[1],patchSize:r}),n.as3D(1,r,e[1]))},a.prototype.dispose=function(){this.model.dispose()},a.prototype.assertMinimumFeatureInputSize=function(t){if(this.minimumInputFrameSize=t.patchSize,t.melSpectrum.length!=this.minimumInputFrameSize)throw Error("When `padding=false` in `predict` method, the model expect audio feature for a minimum frame size of "+this.minimumInputFrameSize+". Was given "+t.melSpectrum.length+" melband frames")},a.prototype.disambiguateExtraInputs=function(){if(!this.isReady)throw Error("No loaded tfjs model found! Make sure to call `initialize` method and resolve the promise before calling `predict` method.");var t=this.model.executor.inputs.length;if(t===1)return[];if(t===2)return[this.IS_TRAIN.clone()];if(t===3)return this.randomTensorInput||(this.randomTensorInput=this.tf.zeros([1,this.model.executor.inputs[0].shape[1]])),[this.randomTensorInput.clone(),this.IS_TRAIN.clone()];throw Error("Found unsupported number of input requirements for the model. Expects the following inputs -> "+this.model.executor.inputs)},a})(),v=(function(a){p(t,a);function t(e,r,s){var n=a.call(this,e,r)||this;return n.minimumInputFrameSize=3,n}return t.prototype.predict=function(e,r){return r===void 0&&(r=!1),c(this,void 0,void 0,function(){var s,n,o,l;return h(this,function(u){switch(u.label){case 0:return s=this.arrayToTensorAsBatches(e.melSpectrum,[e.frameSize,e.melBandsSize],e.patchSize,r),n=this.disambiguateExtraInputs(),n.push(s),o=this.model.execute(n),s.dispose(),[4,o.array()];case 1:return l=u.sent(),o.dispose(),[2,l]}})})},t})(d),T=(function(a){p(t,a);function t(e,r,s){return a.call(this,e,r)||this}return t.prototype.predict=function(e,r){return r===void 0&&(r=!1),c(this,void 0,void 0,function(){var s,n,o,l;return h(this,function(u){switch(u.label){case 0:return s=this.arrayToTensorAsBatches(e.melSpectrum,[e.frameSize,e.melBandsSize],e.patchSize,r),n=this.disambiguateExtraInputs(),n.push(s),o=this.model.execute(n),s.dispose(),[4,o.array()];case 1:return l=u.sent(),o.dispose(),[2,l]}})})},t})(d);exports.EssentiaTFInputExtractor=y;exports.EssentiaTensorflowJSModel=d;exports.TensorflowMusiCNN=v;exports.TensorflowVGGish=T;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @file TF-free gateway entry point.
3
+ *
4
+ * Re-exports only the Arweave gateway manager, Arweave/IPFS URL utilities, and
5
+ * their types. Deliberately does NOT re-export anything from `core/analysis/`
6
+ * (MusicClassifier, EssentiaPitchDetector) or `utils/modelCache.ts`, so that
7
+ * consumers importing from `playlist-data-engine/gateway` do not pull
8
+ * `@tensorflow/tfjs` into their bundle.
9
+ *
10
+ * The full library (including TF-dependent audio analysis) remains available
11
+ * from the main entry: `playlist-data-engine`.
12
+ */
13
+ export { type GatewayConfig, type ArweaveUrlInfo, DEFAULT_GATEWAYS, KNOWN_GATEWAY_HOSTS, isArweaveUrl, parseArweaveUrl, constructGatewayUrl, getAllGatewayUrls, } from './utils/arweaveUtils.js';
14
+ export { ArweaveGatewayManager, arweaveGatewayManager, type GatewayCache, type GatewayCheckResult, type ResolveUrlOptions, type ArweaveGatewayManagerConfig, type PrefetchOptions, type PrefetchResultEntry, type PrefetchResult, type CacheStats, type GatewayHealthStats, type HealthCheckResult, type HealthCheckOptions, type GatewayDiagnostics, } from './utils/arweaveGatewayManager.js';
15
+ export { KNOWN_IPFS_GATEWAY_HOSTS, DEFAULT_IPFS_GATEWAY, extractIPFSPath, isIPFS, resolveIPFSLink, type ResolveIPFSOptions, } from './utils/ipfsUtils.js';
16
+ export { MetadataExtractor } from './core/parser/MetadataExtractor.js';
17
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EACH,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,iBAAiB,GACpB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACH,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,EAChC,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GAC1B,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EACH,wBAAwB,EACxB,oBAAoB,EACpB,eAAe,EACf,MAAM,EACN,eAAe,EACf,KAAK,kBAAkB,GAC1B,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC"}
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("./arweaveGatewayManager-CeW9SP2P.cjs");class l{static extractAudioUrl(t){const e=["mp3_url","mp3Url","lossy_audio","lossyAudio","animation_url","animationUrl","audio_url","audioUrl","lossless_audio","losslessAudio","audio","multimedia_url","multimediaUrl"];for(const r of e)if(t[r]&&typeof t[r]=="string")return t[r];return null}static extractAudioUrlLossless(t){const e=["lossless_audio","losslessAudio","wav_url","wavUrl","flac_url","flacUrl","audio_url","audioUrl"];for(const r of e)if(t[r]&&typeof t[r]=="string")return t[r];return null}static extractImageUrl(t){const e=["image_small","imageSmall","image","image_large","imageLarge","image_thumb","imageThumb","image_url","imageUrl","image_uri","imageUri","image_preview","imagePreview"];for(const i of e)if(t[i]&&typeof t[i]=="string")return t[i];const r=[this.extractNestedUri(t,"artwork","uri"),this.extractNestedUri(t,"project","artwork","uri"),this.extractNestedUri(t,"primaryMedia","uri")];for(const i of r)if(i)return i;return null}static extractImageThumbUrl(t){return t.image_thumb_url&&typeof t.image_thumb_url=="string"?t.image_thumb_url:t.image_thumb&&typeof t.image_thumb=="string"?t.image_thumb:null}static extractTitle(t){return t.name&&typeof t.name=="string"?t.name:t.title&&typeof t.title=="string"?t.title:null}static extractArtist(t){const e=["artist","artist_name","artistName","album_artist","albumArtist","created_by","createdBy","minter"];for(const r of e)if(t[r]&&typeof t[r]=="string")return t[r];if(Array.isArray(t.attributes)){const r=t.attributes.find(i=>i.trait_type?.toLowerCase()==="artist"&&i.value!==void 0);if(r&&typeof r.value=="string")return r.value}return null}static parseMetadata(t){if(!t)return null;if(typeof t=="object"&&!Array.isArray(t))return t;if(typeof t=="string")try{const e=JSON.parse(t);if(typeof e=="object"&&!Array.isArray(e))return e}catch(e){return console.warn("Failed to parse metadata:",e),null}return null}static extractGenre(t){if(typeof t.genre=="string")return t.genre;if(Array.isArray(t.genre)&&t.genre.length>0){const r=t.genre[0];return typeof r=="string"?r:""}if(Array.isArray(t.attributes)){const r=t.attributes.find(i=>i.trait_type?.toLowerCase()==="genre"&&i.value!==void 0);if(r){if(typeof r.value=="string")return r.value;if(Array.isArray(r.value)&&r.value.length>0){const i=r.value[0];return typeof i=="string"?i:""}}}if(t.attributes&&typeof t.attributes=="object"&&!Array.isArray(t.attributes)){const r=t.attributes;for(const i of Object.keys(r))if(i.toLowerCase()==="genre"&&typeof r[i]=="string")return r[i]}const e=t["attribute "];if(e&&typeof e=="object"&&!Array.isArray(e)){const r=e.genre;if(typeof r=="string")return r}return""}static extractTags(t){const e=[];if(Array.isArray(t.tags)?e.push(...t.tags):typeof t.tags=="string"&&e.push(...t.tags.split(",")),t.properties&&typeof t.properties=="object"&&!Array.isArray(t.properties)){const r=t.properties.tags;Array.isArray(r)?e.push(...r):typeof r=="string"&&e.push(...r.split(","))}if(Array.isArray(t.attributes))for(const r of t.attributes){if(!r||typeof r!="object")continue;const i=r.trait_type;if(typeof i=="string"&&i.toLowerCase().match(/^tags?$/)){const n=r.value;Array.isArray(n)?e.push(...n):typeof n=="string"&&e.push(...n.split(","))}}else if(t.attributes&&typeof t.attributes=="object"){const r=t.attributes;for(const i of Object.keys(r))if(i.toLowerCase().match(/^tags?$/)){const n=r[i];Array.isArray(n)?e.push(...n):typeof n=="string"&&e.push(...n.split(","))}}return l.normalizeTags(e)}static normalizeTags(t){const e=new Set;for(const r of t){if(r==null)continue;const i=String(r).trim().toLowerCase();i&&e.add(i)}return Array.from(e)}static convertAttributes(t){if(!Array.isArray(t))return null;const e={};for(const r of t)r.trait_type&&r.value!==void 0&&(e[r.trait_type]=r.value);return Object.keys(e).length>0?e:null}static extractNestedUri(t,...e){let r=t;for(const i of e)if(r&&typeof r=="object"&&!Array.isArray(r))r=r[i];else return null;return typeof r=="string"?r:null}}const u=["ipfs.io","cloudflare-ipfs.com","dweb.link","gateway.pinata.cloud","ipfs.infura.io","nftstorage.link","web3-music-pipeline.mypinata.cloud","soundxyz.mypinata.cloud","gold-broad-gibbon-29.mypinata.cloud","bonfire.mypinata.cloud","spinamp.mypinata.cloud","catalogworks.b-cdn.net","www.heds.cloud"],c="ipfs.io",f="/ipfs/";function y(s){if(!s||typeof s!="string")return null;if(s.startsWith("ipfs://ipfs/"))return s.slice(12);if(s.startsWith("ipfs://"))return s.slice(7);try{const t=new URL(s),{hostname:e,pathname:r}=t,i=e.match(/^([A-Za-z0-9]+)\.ipfs\.dweb\.link$/);if(i)return`${i[1]}${r}`;if(u.some(a=>e===a||e.endsWith("."+a))&&r.startsWith(f))return r.slice(f.length)}catch{}return null}function p(s){if(!s||typeof s!="string")return!1;if(s.startsWith("ipfs://"))return!0;try{const t=new URL(s),{hostname:e,pathname:r}=t;if(/^[A-Za-z0-9]+\.ipfs\.dweb\.link$/.test(e)||u.some(n=>e===n||e.endsWith("."+n))&&r.startsWith(f))return!0}catch{}return!1}function g(s){if(!(!s||s.startsWith("ipfs://")))try{const{hostname:t}=new URL(s);return u.some(r=>t===r||t.endsWith("."+r))?t:void 0}catch{return}}function A(s,t={}){if(!s)return s;const e=typeof t=="string"?{gatewayHost:t}:t,{gatewayHost:r=c,preserveOriginalGateway:i=!1}=e,n=y(s);if(n){if(i){const a=g(s);if(a)return`https://${a}/ipfs/${n}`}return`https://${r}/ipfs/${n}`}return s}exports.ArweaveGatewayManager=o.ArweaveGatewayManager;exports.DEFAULT_GATEWAYS=o.DEFAULT_GATEWAYS;exports.KNOWN_GATEWAY_HOSTS=o.KNOWN_GATEWAY_HOSTS;exports.arweaveGatewayManager=o.arweaveGatewayManager;exports.constructGatewayUrl=o.constructGatewayUrl;exports.getAllGatewayUrls=o.getAllGatewayUrls;exports.isArweaveUrl=o.isArweaveUrl;exports.parseArweaveUrl=o.parseArweaveUrl;exports.DEFAULT_IPFS_GATEWAY=c;exports.KNOWN_IPFS_GATEWAY_HOSTS=u;exports.MetadataExtractor=l;exports.extractIPFSPath=y;exports.isIPFS=p;exports.resolveIPFSLink=A;
@@ -0,0 +1,317 @@
1
+ import { A as h, D as b, K as _, a as w, c as v, g as U, i as T, p as k } from "./arweaveGatewayManager-BKUZ1Klm.js";
2
+ class f {
3
+ /**
4
+ * Extract primary (compressed) audio URL with priority:
5
+ * 1. mp3_url (Standard web audio - preferred)
6
+ * 2. lossy_audio (Compressed)
7
+ * 3. audio_url (Explicit audio field)
8
+ * 4. audio (Bare audio field — some platforms use this instead of audio_url)
9
+ * 5. lossless_audio (High fidelity - larger files)
10
+ * 6. animation_url (OpenSea standard - often audio, but could be video)
11
+ * 7. multimedia_url (Alternative media field)
12
+ */
13
+ static extractAudioUrl(t) {
14
+ const e = ["mp3_url", "mp3Url", "lossy_audio", "lossyAudio", "animation_url", "animationUrl", "audio_url", "audioUrl", "lossless_audio", "losslessAudio", "audio", "multimedia_url", "multimediaUrl"];
15
+ for (const r of e)
16
+ if (t[r] && typeof t[r] == "string")
17
+ return t[r];
18
+ return null;
19
+ }
20
+ /**
21
+ * Extract optional lossless audio URL (uncompressed/high-fidelity source).
22
+ * Returns a lossless URL only if it differs from the primary audio URL.
23
+ * Priority: lossless_audio > animation_url (only if it looks like a lossless format)
24
+ */
25
+ static extractAudioUrlLossless(t) {
26
+ const e = ["lossless_audio", "losslessAudio", "wav_url", "wavUrl", "flac_url", "flacUrl", "audio_url", "audioUrl"];
27
+ for (const r of e)
28
+ if (t[r] && typeof t[r] == "string")
29
+ return t[r];
30
+ return null;
31
+ }
32
+ /**
33
+ * Extract image URL with priority:
34
+ *
35
+ * Flat fields:
36
+ * 1. image_small (Preferred for performance)
37
+ * 2. image (Standard)
38
+ * 3. image_url (Explicit image URL variant)
39
+ * 4. image_large (Fallback)
40
+ * 5. image_uri (URI variant)
41
+ * 6. image_preview (Preview variant)
42
+ * 7. image_thumb (Last resort)
43
+ *
44
+ * Nested object fields (checked if no flat match found):
45
+ * 8. artwork.uri (Nina Protocol structured artwork)
46
+ * 9. project.artwork.uri (Project-level artwork)
47
+ * 10. primaryMedia.uri (Primary media field)
48
+ */
49
+ static extractImageUrl(t) {
50
+ const e = ["image_small", "imageSmall", "image", "image_large", "imageLarge", "image_thumb", "imageThumb", "image_url", "imageUrl", "image_uri", "imageUri", "image_preview", "imagePreview"];
51
+ for (const i of e)
52
+ if (t[i] && typeof t[i] == "string")
53
+ return t[i];
54
+ const r = [
55
+ this.extractNestedUri(t, "artwork", "uri"),
56
+ this.extractNestedUri(t, "project", "artwork", "uri"),
57
+ this.extractNestedUri(t, "primaryMedia", "uri")
58
+ ];
59
+ for (const i of r)
60
+ if (i) return i;
61
+ return null;
62
+ }
63
+ /**
64
+ * Extract thumbnail image URL directly from image_thumb fields.
65
+ * Unlike extractImageUrl, this specifically targets thumbnail fields only.
66
+ * Priority: image_thumb_url > image_thumb
67
+ */
68
+ static extractImageThumbUrl(t) {
69
+ return t.image_thumb_url && typeof t.image_thumb_url == "string" ? t.image_thumb_url : t.image_thumb && typeof t.image_thumb == "string" ? t.image_thumb : null;
70
+ }
71
+ /**
72
+ * Extract name/title with priority (see specs/001-core-engine/SPEC.md):
73
+ * 1. name
74
+ * 2. title
75
+ */
76
+ static extractTitle(t) {
77
+ return t.name && typeof t.name == "string" ? t.name : t.title && typeof t.title == "string" ? t.title : null;
78
+ }
79
+ /**
80
+ * Extract artist with priority (see specs/001-core-engine/SPEC.md):
81
+ * 1. artist
82
+ * 2. created_by
83
+ * 3. minter
84
+ * 4. OpenSea-style attributes array with trait_type "Artist"
85
+ */
86
+ static extractArtist(t) {
87
+ const e = ["artist", "artist_name", "artistName", "album_artist", "albumArtist", "created_by", "createdBy", "minter"];
88
+ for (const r of e)
89
+ if (t[r] && typeof t[r] == "string")
90
+ return t[r];
91
+ if (Array.isArray(t.attributes)) {
92
+ const r = t.attributes.find(
93
+ (i) => i.trait_type?.toLowerCase() === "artist" && i.value !== void 0
94
+ );
95
+ if (r && typeof r.value == "string")
96
+ return r.value;
97
+ }
98
+ return null;
99
+ }
100
+ /**
101
+ * Parse metadata string to JSON object
102
+ * Handles stringified JSON with error handling
103
+ */
104
+ static parseMetadata(t) {
105
+ if (!t)
106
+ return null;
107
+ if (typeof t == "object" && !Array.isArray(t))
108
+ return t;
109
+ if (typeof t == "string")
110
+ try {
111
+ const e = JSON.parse(t);
112
+ if (typeof e == "object" && !Array.isArray(e))
113
+ return e;
114
+ } catch (e) {
115
+ return console.warn("Failed to parse metadata:", e), null;
116
+ }
117
+ return null;
118
+ }
119
+ /**
120
+ * Extract genre with support for string or array formats.
121
+ * Sources are checked in priority order; first non-empty wins:
122
+ * - `genre` field as a string
123
+ * - `genre` field as an array (first element)
124
+ * - OpenSea `attributes` array with `trait_type` of "genre" (case-insensitive)
125
+ * - `attributes` as an object keyed by trait name (e.g. `attributes.Genre`)
126
+ * - malformed `attribute ` key (literal trailing space) with `.genre`
127
+ * Stays strict: only fields literally named "genre" are considered
128
+ * (no fallback to `category` or similar).
129
+ */
130
+ static extractGenre(t) {
131
+ if (typeof t.genre == "string")
132
+ return t.genre;
133
+ if (Array.isArray(t.genre) && t.genre.length > 0) {
134
+ const r = t.genre[0];
135
+ return typeof r == "string" ? r : "";
136
+ }
137
+ if (Array.isArray(t.attributes)) {
138
+ const r = t.attributes.find(
139
+ (i) => i.trait_type?.toLowerCase() === "genre" && i.value !== void 0
140
+ );
141
+ if (r) {
142
+ if (typeof r.value == "string")
143
+ return r.value;
144
+ if (Array.isArray(r.value) && r.value.length > 0) {
145
+ const i = r.value[0];
146
+ return typeof i == "string" ? i : "";
147
+ }
148
+ }
149
+ }
150
+ if (t.attributes && typeof t.attributes == "object" && !Array.isArray(t.attributes)) {
151
+ const r = t.attributes;
152
+ for (const i of Object.keys(r))
153
+ if (i.toLowerCase() === "genre" && typeof r[i] == "string")
154
+ return r[i];
155
+ }
156
+ const e = t["attribute "];
157
+ if (e && typeof e == "object" && !Array.isArray(e)) {
158
+ const r = e.genre;
159
+ if (typeof r == "string")
160
+ return r;
161
+ }
162
+ return "";
163
+ }
164
+ /**
165
+ * Extract tags from track metadata, normalized to a deduped,
166
+ * lowercased, trimmed string[] with empties removed.
167
+ * Sources (concatenated, then deduped):
168
+ * - `tags` field as an array, or a single comma-separated string
169
+ * - `properties.tags` array (when `properties` is an object)
170
+ * - OpenSea `attributes` array with `trait_type` of "tag"/"tags" (case-insensitive)
171
+ * - `attributes` as an object keyed by trait name (e.g. `attributes.Tags`)
172
+ */
173
+ static extractTags(t) {
174
+ const e = [];
175
+ if (Array.isArray(t.tags) ? e.push(...t.tags) : typeof t.tags == "string" && e.push(...t.tags.split(",")), t.properties && typeof t.properties == "object" && !Array.isArray(t.properties)) {
176
+ const r = t.properties.tags;
177
+ Array.isArray(r) ? e.push(...r) : typeof r == "string" && e.push(...r.split(","));
178
+ }
179
+ if (Array.isArray(t.attributes))
180
+ for (const r of t.attributes) {
181
+ if (!r || typeof r != "object") continue;
182
+ const i = r.trait_type;
183
+ if (typeof i == "string" && i.toLowerCase().match(/^tags?$/)) {
184
+ const n = r.value;
185
+ Array.isArray(n) ? e.push(...n) : typeof n == "string" && e.push(...n.split(","));
186
+ }
187
+ }
188
+ else if (t.attributes && typeof t.attributes == "object") {
189
+ const r = t.attributes;
190
+ for (const i of Object.keys(r))
191
+ if (i.toLowerCase().match(/^tags?$/)) {
192
+ const n = r[i];
193
+ Array.isArray(n) ? e.push(...n) : typeof n == "string" && e.push(...n.split(","));
194
+ }
195
+ }
196
+ return f.normalizeTags(e);
197
+ }
198
+ /**
199
+ * Normalize a list of raw tag values into a deduped, lowercased,
200
+ * trimmed string[] with empties removed.
201
+ */
202
+ static normalizeTags(t) {
203
+ const e = /* @__PURE__ */ new Set();
204
+ for (const r of t) {
205
+ if (r == null) continue;
206
+ const i = String(r).trim().toLowerCase();
207
+ i && e.add(i);
208
+ }
209
+ return Array.from(e);
210
+ }
211
+ static convertAttributes(t) {
212
+ if (!Array.isArray(t))
213
+ return null;
214
+ const e = {};
215
+ for (const r of t)
216
+ r.trait_type && r.value !== void 0 && (e[r.trait_type] = r.value);
217
+ return Object.keys(e).length > 0 ? e : null;
218
+ }
219
+ /**
220
+ * Safely extract a URI string from a nested object path.
221
+ * e.g. extractNestedUri(data, 'project', 'artwork', 'uri') => data.project.artwork.uri
222
+ */
223
+ static extractNestedUri(t, ...e) {
224
+ let r = t;
225
+ for (const i of e)
226
+ if (r && typeof r == "object" && !Array.isArray(r))
227
+ r = r[i];
228
+ else
229
+ return null;
230
+ return typeof r == "string" ? r : null;
231
+ }
232
+ }
233
+ const u = [
234
+ "ipfs.io",
235
+ "cloudflare-ipfs.com",
236
+ "dweb.link",
237
+ "gateway.pinata.cloud",
238
+ "ipfs.infura.io",
239
+ "nftstorage.link",
240
+ "web3-music-pipeline.mypinata.cloud",
241
+ "soundxyz.mypinata.cloud",
242
+ "gold-broad-gibbon-29.mypinata.cloud",
243
+ "bonfire.mypinata.cloud",
244
+ "spinamp.mypinata.cloud",
245
+ "catalogworks.b-cdn.net",
246
+ "www.heds.cloud"
247
+ ], l = "ipfs.io", a = "/ipfs/";
248
+ function c(s) {
249
+ if (!s || typeof s != "string") return null;
250
+ if (s.startsWith("ipfs://ipfs/"))
251
+ return s.slice(12);
252
+ if (s.startsWith("ipfs://"))
253
+ return s.slice(7);
254
+ try {
255
+ const t = new URL(s), { hostname: e, pathname: r } = t, i = e.match(/^([A-Za-z0-9]+)\.ipfs\.dweb\.link$/);
256
+ if (i)
257
+ return `${i[1]}${r}`;
258
+ if (u.some(
259
+ (o) => e === o || e.endsWith("." + o)
260
+ ) && r.startsWith(a))
261
+ return r.slice(a.length);
262
+ } catch {
263
+ }
264
+ return null;
265
+ }
266
+ function y(s) {
267
+ if (!s || typeof s != "string") return !1;
268
+ if (s.startsWith("ipfs://")) return !0;
269
+ try {
270
+ const t = new URL(s), { hostname: e, pathname: r } = t;
271
+ if (/^[A-Za-z0-9]+\.ipfs\.dweb\.link$/.test(e) || u.some(
272
+ (n) => e === n || e.endsWith("." + n)
273
+ ) && r.startsWith(a)) return !0;
274
+ } catch {
275
+ }
276
+ return !1;
277
+ }
278
+ function p(s) {
279
+ if (!(!s || s.startsWith("ipfs://")))
280
+ try {
281
+ const { hostname: t } = new URL(s);
282
+ return u.some(
283
+ (r) => t === r || t.endsWith("." + r)
284
+ ) ? t : void 0;
285
+ } catch {
286
+ return;
287
+ }
288
+ }
289
+ function g(s, t = {}) {
290
+ if (!s) return s;
291
+ const e = typeof t == "string" ? { gatewayHost: t } : t, { gatewayHost: r = l, preserveOriginalGateway: i = !1 } = e, n = c(s);
292
+ if (n) {
293
+ if (i) {
294
+ const o = p(s);
295
+ if (o)
296
+ return `https://${o}/ipfs/${n}`;
297
+ }
298
+ return `https://${r}/ipfs/${n}`;
299
+ }
300
+ return s;
301
+ }
302
+ export {
303
+ h as ArweaveGatewayManager,
304
+ b as DEFAULT_GATEWAYS,
305
+ l as DEFAULT_IPFS_GATEWAY,
306
+ _ as KNOWN_GATEWAY_HOSTS,
307
+ u as KNOWN_IPFS_GATEWAY_HOSTS,
308
+ f as MetadataExtractor,
309
+ w as arweaveGatewayManager,
310
+ v as constructGatewayUrl,
311
+ c as extractIPFSPath,
312
+ U as getAllGatewayUrls,
313
+ T as isArweaveUrl,
314
+ y as isIPFS,
315
+ k as parseArweaveUrl,
316
+ g as resolveIPFSLink
317
+ };