gopeak 2.0.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,174 @@
1
+ import { existsSync, mkdirSync, createWriteStream, appendFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import * as https from 'https';
4
+ const AMBIENTCG_CONFIG = {
5
+ name: 'ambientcg',
6
+ displayName: 'AmbientCG',
7
+ baseUrl: 'https://ambientcg.com',
8
+ apiUrl: 'https://ambientcg.com/api/v2/full_json',
9
+ userAgent: 'GodotMCP/1.0',
10
+ priority: 2,
11
+ supportedTypes: ['textures', 'models', 'hdris'],
12
+ requiresAuth: false,
13
+ license: 'CC0',
14
+ };
15
+ function mapDataTypeToAssetType(dataType) {
16
+ switch (dataType.toLowerCase()) {
17
+ case 'material':
18
+ case 'decal':
19
+ case 'terrain':
20
+ return 'textures';
21
+ case '3dmodel':
22
+ return 'models';
23
+ case 'hdri':
24
+ return 'hdris';
25
+ default:
26
+ return 'textures';
27
+ }
28
+ }
29
+ function httpGet(url, userAgent) {
30
+ return new Promise((resolve, reject) => {
31
+ https.get(url, { headers: { 'User-Agent': userAgent } }, (res) => {
32
+ if (res.statusCode === 301 || res.statusCode === 302) {
33
+ const redirectUrl = res.headers.location;
34
+ if (redirectUrl) {
35
+ httpGet(redirectUrl, userAgent).then(resolve).catch(reject);
36
+ return;
37
+ }
38
+ }
39
+ let data = '';
40
+ res.on('data', (chunk) => data += chunk);
41
+ res.on('end', () => resolve(data));
42
+ }).on('error', reject);
43
+ });
44
+ }
45
+ function downloadFile(url, destPath, userAgent) {
46
+ return new Promise((resolve, reject) => {
47
+ const file = createWriteStream(destPath);
48
+ const request = (currentUrl) => {
49
+ https.get(currentUrl, { headers: { 'User-Agent': userAgent } }, (response) => {
50
+ if (response.statusCode === 301 || response.statusCode === 302) {
51
+ const redirectUrl = response.headers.location;
52
+ if (redirectUrl) {
53
+ request(redirectUrl);
54
+ return;
55
+ }
56
+ }
57
+ response.pipe(file);
58
+ file.on('finish', () => {
59
+ file.close();
60
+ resolve();
61
+ });
62
+ }).on('error', (err) => {
63
+ file.close();
64
+ reject(err);
65
+ });
66
+ };
67
+ request(url);
68
+ });
69
+ }
70
+ export class AmbientCGProvider {
71
+ config = AMBIENTCG_CONFIG;
72
+ supportsType(type) {
73
+ return this.config.supportedTypes.includes(type);
74
+ }
75
+ getAttribution(assetId, assetName) {
76
+ return `## ${assetName}\n- Source: [AmbientCG](https://ambientcg.com/view?id=${assetId})\n- License: CC0 (Public Domain)\n- Downloaded: ${new Date().toISOString()}\n`;
77
+ }
78
+ async search(options) {
79
+ const { keyword, assetType, maxResults = 10 } = options;
80
+ let typeFilter = '';
81
+ if (assetType === 'textures') {
82
+ typeFilter = '&type=Material,Decal,Terrain';
83
+ }
84
+ else if (assetType === 'models') {
85
+ typeFilter = '&type=3DModel';
86
+ }
87
+ else if (assetType === 'hdris') {
88
+ typeFilter = '&type=HDRI';
89
+ }
90
+ const searchUrl = `${this.config.apiUrl}?q=${encodeURIComponent(keyword)}&limit=${maxResults}${typeFilter}&include=downloadData,previewData`;
91
+ try {
92
+ const responseText = await httpGet(searchUrl, this.config.userAgent);
93
+ const response = JSON.parse(responseText);
94
+ if (!response.foundAssets || response.foundAssets.length === 0) {
95
+ return [];
96
+ }
97
+ return response.foundAssets.map((asset, index) => {
98
+ const previewUrl = asset.previewImage?.['256-PNG'] || asset.previewImage?.['512-PNG'] || '';
99
+ return {
100
+ id: asset.assetId,
101
+ name: asset.assetId.replace(/_/g, ' '),
102
+ provider: this.config.name,
103
+ assetType: mapDataTypeToAssetType(asset.dataType),
104
+ categories: asset.category ? [asset.category] : [],
105
+ tags: asset.tags || [],
106
+ license: 'CC0',
107
+ previewUrl,
108
+ downloadUrl: asset.downloadLink,
109
+ score: 100 - index,
110
+ };
111
+ });
112
+ }
113
+ catch (error) {
114
+ console.error(`[AmbientCG] Search failed: ${error}`);
115
+ return [];
116
+ }
117
+ }
118
+ async download(options) {
119
+ const { assetId, projectPath, targetFolder = 'downloaded_assets/ambientcg', resolution = '2K' } = options;
120
+ const detailUrl = `${this.config.apiUrl}?id=${assetId}&include=downloadData`;
121
+ try {
122
+ const responseText = await httpGet(detailUrl, this.config.userAgent);
123
+ const response = JSON.parse(responseText);
124
+ if (!response.foundAssets || response.foundAssets.length === 0) {
125
+ throw new Error(`Asset not found: ${assetId}`);
126
+ }
127
+ const asset = response.foundAssets[0];
128
+ let downloadUrl = asset.downloadLink;
129
+ if (asset.rawLink) {
130
+ downloadUrl = asset.rawLink;
131
+ }
132
+ if (!downloadUrl) {
133
+ throw new Error(`No download URL for asset: ${assetId}`);
134
+ }
135
+ const targetDir = join(projectPath, targetFolder);
136
+ if (!existsSync(targetDir)) {
137
+ mkdirSync(targetDir, { recursive: true });
138
+ }
139
+ const fileName = `${assetId}.zip`;
140
+ const filePath = join(targetDir, fileName);
141
+ await downloadFile(downloadUrl, filePath, this.config.userAgent);
142
+ const creditsPath = join(targetDir, 'CREDITS.md');
143
+ const attribution = this.getAttribution(assetId, assetId);
144
+ if (existsSync(creditsPath)) {
145
+ appendFileSync(creditsPath, '\n' + attribution);
146
+ }
147
+ else {
148
+ writeFileSync(creditsPath, `# Asset Credits\n\nAssets downloaded from various CC0 sources\n\n${attribution}`);
149
+ }
150
+ return {
151
+ success: true,
152
+ assetId,
153
+ provider: this.config.name,
154
+ localPath: `${targetFolder}/${fileName}`,
155
+ fileName,
156
+ license: 'CC0',
157
+ attribution,
158
+ sourceUrl: `https://ambientcg.com/view?id=${assetId}`,
159
+ };
160
+ }
161
+ catch (error) {
162
+ return {
163
+ success: false,
164
+ assetId,
165
+ provider: this.config.name,
166
+ localPath: '',
167
+ fileName: '',
168
+ license: 'CC0',
169
+ attribution: '',
170
+ sourceUrl: `https://ambientcg.com/view?id=${assetId}`,
171
+ };
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export * from './polyhaven.js';
3
+ export * from './ambientcg.js';
4
+ export * from './kenney.js';
5
+ export * from './manager.js';
@@ -0,0 +1,159 @@
1
+ import { existsSync, mkdirSync, createWriteStream, appendFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import * as https from 'https';
4
+ const KENNEY_CONFIG = {
5
+ name: 'kenney',
6
+ displayName: 'Kenney',
7
+ baseUrl: 'https://kenney.nl',
8
+ userAgent: 'GodotMCP/1.0',
9
+ priority: 3,
10
+ supportedTypes: ['models', 'textures', '2d', 'audio'],
11
+ requiresAuth: false,
12
+ license: 'CC0',
13
+ };
14
+ const KENNEY_ASSET_PACKS = [
15
+ { id: '3d-city', name: '3D City', category: 'Buildings', assetType: 'models', tags: ['city', 'urban', 'building', 'road', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/3d-city/2282f0a7e4-1728559116/kenney_3d-city.zip' },
16
+ { id: '3d-nature', name: '3D Nature Pack', category: 'Nature', assetType: 'models', tags: ['nature', 'tree', 'plant', 'rock', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/nature-kit/ed10aaba45-1728559116/kenney_nature-kit.zip' },
17
+ { id: 'furniture-kit', name: 'Furniture Kit', category: 'Interior', assetType: 'models', tags: ['furniture', 'chair', 'table', 'interior', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/furniture-kit/3e16e37fc1-1728559116/kenney_furniture-kit.zip' },
18
+ { id: 'tower-defense', name: 'Tower Defense Kit', category: 'Game', assetType: 'models', tags: ['tower', 'defense', 'game', 'strategy', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/tower-defense-kit/4a4ef34e87-1728559116/kenney_tower-defense-kit.zip' },
19
+ { id: 'medieval-rts', name: 'Medieval RTS', category: 'Medieval', assetType: 'models', tags: ['medieval', 'castle', 'knight', 'rts', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/medieval-rts/1c03b7bec3-1728559116/kenney_medieval-rts.zip' },
20
+ { id: 'racing-kit', name: 'Racing Kit', category: 'Vehicle', assetType: 'models', tags: ['car', 'racing', 'vehicle', 'road', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/racing-kit/94e7a5c4e8-1728559116/kenney_racing-kit.zip' },
21
+ { id: 'space-kit', name: 'Space Kit', category: 'Space', assetType: 'models', tags: ['space', 'rocket', 'planet', 'spaceship', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/space-kit/0c8c35e2c9-1728559116/kenney_space-kit.zip' },
22
+ { id: 'pirate-kit', name: 'Pirate Kit', category: 'Fantasy', assetType: 'models', tags: ['pirate', 'ship', 'treasure', 'island', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/pirate-kit/8f0ced50f7-1728559116/kenney_pirate-kit.zip' },
23
+ { id: 'food-kit', name: 'Food Kit', category: 'Props', assetType: 'models', tags: ['food', 'fruit', 'vegetable', 'kitchen', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/food-kit/9d0a0bf3d7-1728559116/kenney_food-kit.zip' },
24
+ { id: 'holiday-kit', name: 'Holiday Kit', category: 'Holiday', assetType: 'models', tags: ['christmas', 'holiday', 'decoration', 'winter', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/holiday-kit/b1c8fe2d1f-1728559116/kenney_holiday-kit.zip' },
25
+ { id: 'platformer-kit', name: 'Platformer Kit', category: 'Game', assetType: 'models', tags: ['platformer', 'game', 'tiles', 'character', 'low-poly'], downloadUrl: 'https://kenney.nl/media/pages/assets/platformer-kit/f3a7e8d2c1-1728559116/kenney_platformer-kit.zip' },
26
+ { id: 'monochrome-rpg', name: 'Monochrome RPG', category: '2D', assetType: '2d', tags: ['rpg', 'pixel', 'monochrome', 'character', 'tileset'], downloadUrl: 'https://kenney.nl/media/pages/assets/monochrome-rpg/8d3b2f1e4a-1728559116/kenney_monochrome-rpg.zip' },
27
+ { id: 'tiny-dungeon', name: 'Tiny Dungeon', category: '2D', assetType: '2d', tags: ['dungeon', 'pixel', 'tiles', 'character', 'fantasy'], downloadUrl: 'https://kenney.nl/media/pages/assets/tiny-dungeon/a2c4e6f8d0-1728559116/kenney_tiny-dungeon.zip' },
28
+ { id: 'particle-pack', name: 'Particle Pack', category: 'Effects', assetType: 'textures', tags: ['particle', 'effect', 'smoke', 'fire', 'explosion'], downloadUrl: 'https://kenney.nl/media/pages/assets/particle-pack/1b3d5f7a9c-1728559116/kenney_particle-pack.zip' },
29
+ { id: 'ui-pack', name: 'UI Pack', category: 'UI', assetType: '2d', tags: ['ui', 'button', 'interface', 'menu', 'icon'], downloadUrl: 'https://kenney.nl/media/pages/assets/ui-pack/e4f6a8c0d2-1728559116/kenney_ui-pack.zip' },
30
+ { id: 'impact-sounds', name: 'Impact Sounds', category: 'Audio', assetType: 'audio', tags: ['impact', 'sound', 'effect', 'hit', 'sfx'], downloadUrl: 'https://kenney.nl/media/pages/assets/impact-sounds/5d7f9a1c3e-1728559116/kenney_impact-sounds.zip' },
31
+ { id: 'rpg-sounds', name: 'RPG Sounds', category: 'Audio', assetType: 'audio', tags: ['rpg', 'sound', 'effect', 'game', 'sfx'], downloadUrl: 'https://kenney.nl/media/pages/assets/rpg-audio/b6d8e0f2a4-1728559116/kenney_rpg-audio.zip' },
32
+ ];
33
+ function downloadFile(url, destPath, userAgent) {
34
+ return new Promise((resolve, reject) => {
35
+ const file = createWriteStream(destPath);
36
+ const request = (currentUrl) => {
37
+ https.get(currentUrl, { headers: { 'User-Agent': userAgent } }, (response) => {
38
+ if (response.statusCode === 301 || response.statusCode === 302) {
39
+ const redirectUrl = response.headers.location;
40
+ if (redirectUrl) {
41
+ request(redirectUrl);
42
+ return;
43
+ }
44
+ }
45
+ response.pipe(file);
46
+ file.on('finish', () => {
47
+ file.close();
48
+ resolve();
49
+ });
50
+ }).on('error', (err) => {
51
+ file.close();
52
+ reject(err);
53
+ });
54
+ };
55
+ request(url);
56
+ });
57
+ }
58
+ export class KenneyProvider {
59
+ config = KENNEY_CONFIG;
60
+ supportsType(type) {
61
+ return this.config.supportedTypes.includes(type);
62
+ }
63
+ getAttribution(assetId, assetName) {
64
+ return `## ${assetName}\n- Source: [Kenney](https://kenney.nl/assets/${assetId})\n- License: CC0 (Public Domain)\n- Downloaded: ${new Date().toISOString()}\n`;
65
+ }
66
+ async search(options) {
67
+ const { keyword, assetType, maxResults = 10 } = options;
68
+ const keywordLower = keyword.toLowerCase();
69
+ let filtered = KENNEY_ASSET_PACKS;
70
+ if (assetType) {
71
+ filtered = filtered.filter(pack => pack.assetType === assetType);
72
+ }
73
+ const scored = filtered.map(pack => {
74
+ let score = 0;
75
+ if (pack.id.toLowerCase().includes(keywordLower))
76
+ score += 100;
77
+ if (pack.name.toLowerCase().includes(keywordLower))
78
+ score += 80;
79
+ if (pack.category.toLowerCase().includes(keywordLower))
80
+ score += 50;
81
+ for (const tag of pack.tags) {
82
+ if (tag.toLowerCase().includes(keywordLower))
83
+ score += 30;
84
+ }
85
+ return { ...pack, score };
86
+ });
87
+ const matches = scored
88
+ .filter(pack => pack.score > 0)
89
+ .sort((a, b) => b.score - a.score)
90
+ .slice(0, maxResults);
91
+ return matches.map(pack => ({
92
+ id: pack.id,
93
+ name: pack.name,
94
+ provider: this.config.name,
95
+ assetType: pack.assetType,
96
+ categories: [pack.category],
97
+ tags: pack.tags,
98
+ license: 'CC0',
99
+ previewUrl: `https://kenney.nl/media/pages/assets/${pack.id}/preview.png`,
100
+ downloadUrl: pack.downloadUrl,
101
+ score: pack.score,
102
+ }));
103
+ }
104
+ async download(options) {
105
+ const { assetId, projectPath, targetFolder = 'downloaded_assets/kenney' } = options;
106
+ const pack = KENNEY_ASSET_PACKS.find(p => p.id === assetId);
107
+ if (!pack) {
108
+ return {
109
+ success: false,
110
+ assetId,
111
+ provider: this.config.name,
112
+ localPath: '',
113
+ fileName: '',
114
+ license: 'CC0',
115
+ attribution: '',
116
+ sourceUrl: `https://kenney.nl/assets/${assetId}`,
117
+ };
118
+ }
119
+ try {
120
+ const targetDir = join(projectPath, targetFolder);
121
+ if (!existsSync(targetDir)) {
122
+ mkdirSync(targetDir, { recursive: true });
123
+ }
124
+ const fileName = `kenney_${assetId}.zip`;
125
+ const filePath = join(targetDir, fileName);
126
+ await downloadFile(pack.downloadUrl, filePath, this.config.userAgent);
127
+ const creditsPath = join(targetDir, 'CREDITS.md');
128
+ const attribution = this.getAttribution(assetId, pack.name);
129
+ if (existsSync(creditsPath)) {
130
+ appendFileSync(creditsPath, '\n' + attribution);
131
+ }
132
+ else {
133
+ writeFileSync(creditsPath, `# Asset Credits\n\nAssets downloaded from Kenney (CC0 License)\n\n${attribution}`);
134
+ }
135
+ return {
136
+ success: true,
137
+ assetId,
138
+ provider: this.config.name,
139
+ localPath: `${targetFolder}/${fileName}`,
140
+ fileName,
141
+ license: 'CC0',
142
+ attribution,
143
+ sourceUrl: `https://kenney.nl/assets/${assetId}`,
144
+ };
145
+ }
146
+ catch (error) {
147
+ return {
148
+ success: false,
149
+ assetId,
150
+ provider: this.config.name,
151
+ localPath: '',
152
+ fileName: '',
153
+ license: 'CC0',
154
+ attribution: '',
155
+ sourceUrl: `https://kenney.nl/assets/${assetId}`,
156
+ };
157
+ }
158
+ }
159
+ }
@@ -0,0 +1,122 @@
1
+ import { PROVIDER_PRIORITY, } from './types.js';
2
+ import { PolyHavenProvider } from './polyhaven.js';
3
+ import { AmbientCGProvider } from './ambientcg.js';
4
+ import { KenneyProvider } from './kenney.js';
5
+ export class AssetManager {
6
+ providers = new Map();
7
+ sortedProviders = [];
8
+ constructor() {
9
+ this.registerProvider(new PolyHavenProvider());
10
+ this.registerProvider(new AmbientCGProvider());
11
+ this.registerProvider(new KenneyProvider());
12
+ this.sortProvidersByPriority();
13
+ }
14
+ registerProvider(provider) {
15
+ this.providers.set(provider.config.name, provider);
16
+ }
17
+ sortProvidersByPriority() {
18
+ this.sortedProviders = Array.from(this.providers.values()).sort((a, b) => (PROVIDER_PRIORITY[a.config.name] || 99) - (PROVIDER_PRIORITY[b.config.name] || 99));
19
+ }
20
+ getProviderNames() {
21
+ return this.sortedProviders.map(p => p.config.name);
22
+ }
23
+ getProviderInfo() {
24
+ return this.sortedProviders.map(p => ({
25
+ name: p.config.name,
26
+ displayName: p.config.displayName,
27
+ priority: p.config.priority,
28
+ types: p.config.supportedTypes,
29
+ }));
30
+ }
31
+ async searchAll(options) {
32
+ const { assetType, maxResults = 10 } = options;
33
+ const allResults = [];
34
+ for (const provider of this.sortedProviders) {
35
+ if (assetType && !provider.supportsType(assetType)) {
36
+ continue;
37
+ }
38
+ try {
39
+ const results = await provider.search(options);
40
+ allResults.push(...results);
41
+ }
42
+ catch (error) {
43
+ console.error(`[AssetManager] Search failed for ${provider.config.name}: ${error}`);
44
+ }
45
+ }
46
+ return allResults
47
+ .sort((a, b) => {
48
+ const priorityA = PROVIDER_PRIORITY[a.provider] || 99;
49
+ const priorityB = PROVIDER_PRIORITY[b.provider] || 99;
50
+ if (priorityA !== priorityB)
51
+ return priorityA - priorityB;
52
+ return b.score - a.score;
53
+ })
54
+ .slice(0, maxResults);
55
+ }
56
+ async searchSequential(options) {
57
+ const { assetType, maxResults = 5 } = options;
58
+ for (const provider of this.sortedProviders) {
59
+ if (assetType && !provider.supportsType(assetType)) {
60
+ continue;
61
+ }
62
+ try {
63
+ const results = await provider.search({ ...options, maxResults });
64
+ if (results.length > 0) {
65
+ return results;
66
+ }
67
+ }
68
+ catch (error) {
69
+ console.error(`[AssetManager] Search failed for ${provider.config.name}: ${error}`);
70
+ }
71
+ }
72
+ return [];
73
+ }
74
+ async searchProvider(providerName, options) {
75
+ const provider = this.providers.get(providerName);
76
+ if (!provider) {
77
+ throw new Error(`Provider not found: ${providerName}`);
78
+ }
79
+ return provider.search(options);
80
+ }
81
+ async download(options) {
82
+ const { provider: providerName, assetId } = options;
83
+ if (providerName) {
84
+ const provider = this.providers.get(providerName);
85
+ if (!provider) {
86
+ return {
87
+ success: false,
88
+ assetId,
89
+ provider: providerName,
90
+ localPath: '',
91
+ fileName: '',
92
+ license: '',
93
+ attribution: '',
94
+ sourceUrl: '',
95
+ };
96
+ }
97
+ return provider.download(options);
98
+ }
99
+ for (const provider of this.sortedProviders) {
100
+ try {
101
+ const result = await provider.download(options);
102
+ if (result.success) {
103
+ return result;
104
+ }
105
+ }
106
+ catch (error) {
107
+ console.error(`[AssetManager] Download failed for ${provider.config.name}: ${error}`);
108
+ }
109
+ }
110
+ return {
111
+ success: false,
112
+ assetId,
113
+ provider: 'unknown',
114
+ localPath: '',
115
+ fileName: '',
116
+ license: '',
117
+ attribution: '',
118
+ sourceUrl: '',
119
+ };
120
+ }
121
+ }
122
+ export const assetManager = new AssetManager();
@@ -0,0 +1,191 @@
1
+ import { existsSync, mkdirSync, createWriteStream, appendFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import * as https from 'https';
4
+ const POLYHAVEN_CONFIG = {
5
+ name: 'polyhaven',
6
+ displayName: 'Poly Haven',
7
+ baseUrl: 'https://polyhaven.com',
8
+ apiUrl: 'https://api.polyhaven.com',
9
+ userAgent: 'GodotMCP/1.0',
10
+ priority: 1,
11
+ supportedTypes: ['models', 'textures', 'hdris'],
12
+ requiresAuth: false,
13
+ license: 'CC0',
14
+ };
15
+ function httpGet(url, userAgent) {
16
+ return new Promise((resolve, reject) => {
17
+ https.get(url, { headers: { 'User-Agent': userAgent } }, (res) => {
18
+ if (res.statusCode === 301 || res.statusCode === 302) {
19
+ const redirectUrl = res.headers.location;
20
+ if (redirectUrl) {
21
+ httpGet(redirectUrl, userAgent).then(resolve).catch(reject);
22
+ return;
23
+ }
24
+ }
25
+ let data = '';
26
+ res.on('data', (chunk) => data += chunk);
27
+ res.on('end', () => resolve(data));
28
+ }).on('error', reject);
29
+ });
30
+ }
31
+ function downloadFile(url, destPath, userAgent) {
32
+ return new Promise((resolve, reject) => {
33
+ const file = createWriteStream(destPath);
34
+ const request = (currentUrl) => {
35
+ https.get(currentUrl, { headers: { 'User-Agent': userAgent } }, (response) => {
36
+ if (response.statusCode === 301 || response.statusCode === 302) {
37
+ const redirectUrl = response.headers.location;
38
+ if (redirectUrl) {
39
+ request(redirectUrl);
40
+ return;
41
+ }
42
+ }
43
+ response.pipe(file);
44
+ file.on('finish', () => {
45
+ file.close();
46
+ resolve();
47
+ });
48
+ }).on('error', (err) => {
49
+ file.close();
50
+ reject(err);
51
+ });
52
+ };
53
+ request(url);
54
+ });
55
+ }
56
+ function mapAssetTypeToApiParam(type) {
57
+ switch (type) {
58
+ case 'models': return 'models';
59
+ case 'textures': return 'textures';
60
+ case 'hdris': return 'hdris';
61
+ default: return 'models';
62
+ }
63
+ }
64
+ export class PolyHavenProvider {
65
+ config = POLYHAVEN_CONFIG;
66
+ supportsType(type) {
67
+ return this.config.supportedTypes.includes(type);
68
+ }
69
+ getAttribution(assetId, assetName) {
70
+ return `## ${assetName}\n- Source: [Poly Haven](https://polyhaven.com/a/${assetId})\n- License: CC0 (Public Domain)\n- Downloaded: ${new Date().toISOString()}\n`;
71
+ }
72
+ async search(options) {
73
+ const { keyword, assetType = 'models', maxResults = 10 } = options;
74
+ const apiType = mapAssetTypeToApiParam(assetType);
75
+ const searchUrl = `${this.config.apiUrl}/assets?t=${apiType}`;
76
+ try {
77
+ const responseText = await httpGet(searchUrl, this.config.userAgent);
78
+ const allAssets = JSON.parse(responseText);
79
+ const keywordLower = keyword.toLowerCase();
80
+ const matches = [];
81
+ for (const [assetId, assetData] of Object.entries(allAssets)) {
82
+ let score = 0;
83
+ if (assetId.toLowerCase().includes(keywordLower))
84
+ score += 100;
85
+ if (assetData.name?.toLowerCase().includes(keywordLower))
86
+ score += 80;
87
+ if (assetData.tags) {
88
+ for (const tag of assetData.tags) {
89
+ if (tag.toLowerCase().includes(keywordLower))
90
+ score += 50;
91
+ }
92
+ }
93
+ if (assetData.categories) {
94
+ for (const cat of assetData.categories) {
95
+ if (cat.toLowerCase().includes(keywordLower))
96
+ score += 30;
97
+ }
98
+ }
99
+ if (score > 0) {
100
+ matches.push({
101
+ id: assetId,
102
+ name: assetData.name || assetId,
103
+ provider: this.config.name,
104
+ assetType,
105
+ categories: assetData.categories || [],
106
+ tags: assetData.tags || [],
107
+ license: 'CC0',
108
+ previewUrl: `https://cdn.polyhaven.com/asset_img/thumbs/${assetId}.png`,
109
+ score,
110
+ });
111
+ }
112
+ }
113
+ return matches
114
+ .sort((a, b) => b.score - a.score)
115
+ .slice(0, maxResults);
116
+ }
117
+ catch (error) {
118
+ console.error(`[PolyHaven] Search failed: ${error}`);
119
+ return [];
120
+ }
121
+ }
122
+ async download(options) {
123
+ const { assetId, projectPath, targetFolder = 'downloaded_assets/polyhaven', resolution = '2k', format, } = options;
124
+ try {
125
+ const fileInfoUrl = `${this.config.apiUrl}/files/${assetId}`;
126
+ const responseText = await httpGet(fileInfoUrl, this.config.userAgent);
127
+ const fileInfo = JSON.parse(responseText);
128
+ let downloadUrl = '';
129
+ let fileExtension = '.glb';
130
+ let detectedType = 'models';
131
+ if (fileInfo.gltf) {
132
+ detectedType = 'models';
133
+ const res = fileInfo.gltf[resolution] || fileInfo.gltf[Object.keys(fileInfo.gltf)[0]];
134
+ downloadUrl = res?.gltf?.url || '';
135
+ fileExtension = '.glb';
136
+ }
137
+ else if (fileInfo.Diffuse) {
138
+ detectedType = 'textures';
139
+ const res = fileInfo.Diffuse[resolution] || fileInfo.Diffuse[Object.keys(fileInfo.Diffuse)[0]];
140
+ downloadUrl = res?.png?.url || '';
141
+ fileExtension = '.png';
142
+ }
143
+ else if (fileInfo.hdri) {
144
+ detectedType = 'hdris';
145
+ const res = fileInfo.hdri[resolution] || fileInfo.hdri[Object.keys(fileInfo.hdri)[0]];
146
+ downloadUrl = res?.hdr?.url || '';
147
+ fileExtension = '.hdr';
148
+ }
149
+ if (!downloadUrl) {
150
+ throw new Error(`No download URL found for asset: ${assetId}`);
151
+ }
152
+ const targetDir = join(projectPath, targetFolder);
153
+ if (!existsSync(targetDir)) {
154
+ mkdirSync(targetDir, { recursive: true });
155
+ }
156
+ const fileName = `${assetId}${fileExtension}`;
157
+ const filePath = join(targetDir, fileName);
158
+ await downloadFile(downloadUrl, filePath, this.config.userAgent);
159
+ const creditsPath = join(targetDir, 'CREDITS.md');
160
+ const attribution = this.getAttribution(assetId, assetId);
161
+ if (existsSync(creditsPath)) {
162
+ appendFileSync(creditsPath, '\n' + attribution);
163
+ }
164
+ else {
165
+ writeFileSync(creditsPath, `# Asset Credits\n\nAssets downloaded from Poly Haven (CC0 License)\n\n${attribution}`);
166
+ }
167
+ return {
168
+ success: true,
169
+ assetId,
170
+ provider: this.config.name,
171
+ localPath: `${targetFolder}/${fileName}`,
172
+ fileName,
173
+ license: 'CC0',
174
+ attribution,
175
+ sourceUrl: `https://polyhaven.com/a/${assetId}`,
176
+ };
177
+ }
178
+ catch (error) {
179
+ return {
180
+ success: false,
181
+ assetId,
182
+ provider: this.config.name,
183
+ localPath: '',
184
+ fileName: '',
185
+ license: 'CC0',
186
+ attribution: '',
187
+ sourceUrl: `https://polyhaven.com/a/${assetId}`,
188
+ };
189
+ }
190
+ }
191
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Asset Provider Interface & Types
3
+ *
4
+ * This module defines the common interface for all asset providers (Poly Haven, AmbientCG, etc.)
5
+ * enabling a unified multi-source asset search and download system.
6
+ */
7
+ /**
8
+ * Provider priority order (used for sequential search)
9
+ */
10
+ export const PROVIDER_PRIORITY = {
11
+ 'polyhaven': 1,
12
+ 'ambientcg': 2,
13
+ 'kenney': 3,
14
+ 'cc0lib': 4,
15
+ 'quaternius': 5,
16
+ 'nasa3d': 6,
17
+ 'opengameart': 7,
18
+ 'smithsonian': 8,
19
+ };