ff1-cli 1.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.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/config.json.example +78 -0
- package/dist/index.js +627 -0
- package/dist/src/ai-orchestrator/index.js +870 -0
- package/dist/src/ai-orchestrator/registry.js +96 -0
- package/dist/src/config.js +352 -0
- package/dist/src/intent-parser/index.js +1342 -0
- package/dist/src/intent-parser/utils.js +108 -0
- package/dist/src/logger.js +72 -0
- package/dist/src/main.js +393 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utilities/address-validator.js +242 -0
- package/dist/src/utilities/domain-resolver.js +291 -0
- package/dist/src/utilities/feed-fetcher.js +387 -0
- package/dist/src/utilities/ff1-device.js +176 -0
- package/dist/src/utilities/functions.js +325 -0
- package/dist/src/utilities/index.js +372 -0
- package/dist/src/utilities/nft-indexer.js +1013 -0
- package/dist/src/utilities/playlist-builder.js +522 -0
- package/dist/src/utilities/playlist-publisher.js +131 -0
- package/dist/src/utilities/playlist-send.js +241 -0
- package/dist/src/utilities/playlist-signer.js +171 -0
- package/dist/src/utilities/playlist-verifier.js +156 -0
- package/dist/src/utils.js +48 -0
- package/docs/CONFIGURATION.md +178 -0
- package/docs/EXAMPLES.md +331 -0
- package/docs/FUNCTION_CALLING.md +92 -0
- package/docs/README.md +267 -0
- package/docs/RELEASING.md +22 -0
- package/package.json +75 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DP1 Feed Fetcher
|
|
3
|
+
* Utilities to fetch playlists from DP1 Feed API
|
|
4
|
+
* API: https://github.com/display-protocol/dp1-feed
|
|
5
|
+
*/
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const fuzzysort = require('fuzzysort');
|
|
8
|
+
const { getFeedConfig } = require('../config');
|
|
9
|
+
/**
|
|
10
|
+
* Get feed API base URLs from configuration
|
|
11
|
+
*
|
|
12
|
+
* @returns {string[]} Array of feed API base URLs
|
|
13
|
+
*/
|
|
14
|
+
function getFeedApiUrls() {
|
|
15
|
+
const feedConfig = getFeedConfig();
|
|
16
|
+
return feedConfig.baseURLs;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fetch playlists from a single feed URL with pagination
|
|
20
|
+
*
|
|
21
|
+
* @param {string} feedUrl - Feed API base URL
|
|
22
|
+
* @param {number} limit - Items per page (default: 50, max: 100)
|
|
23
|
+
* @returns {Promise<Array>} Array of playlists
|
|
24
|
+
*/
|
|
25
|
+
async function fetchPlaylistsFromFeed(feedUrl, limit = 100) {
|
|
26
|
+
try {
|
|
27
|
+
// API has a maximum limit of 100
|
|
28
|
+
const validLimit = Math.min(limit, 100);
|
|
29
|
+
const response = await fetch(`${feedUrl}/playlists?limit=${validLimit}&sort=-created`);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
console.log(chalk.yellow(` ⚠️ Feed ${feedUrl} returned ${response.status}`));
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
const playlists = data.items || [];
|
|
36
|
+
// Add feedUrl to each playlist for tracking
|
|
37
|
+
return playlists.map((p) => ({
|
|
38
|
+
...p,
|
|
39
|
+
feedUrl,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.log(chalk.yellow(` ⚠️ Failed to fetch from ${feedUrl}: ${error.message}`));
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Fetch playlists with pagination and fuzzy filtering to save memory
|
|
49
|
+
*
|
|
50
|
+
* @param {string} feedUrl - Feed API base URL
|
|
51
|
+
* @param {string} searchTerm - Search term for fuzzy filtering
|
|
52
|
+
* @param {number} pageSize - Items per page (default: 50, max: 100)
|
|
53
|
+
* @param {number} topN - Keep top N matches per page (default: 10)
|
|
54
|
+
* @param {number} maxItems - Maximum total items to fetch (default: 500)
|
|
55
|
+
* @returns {Promise<Array>} Array of best matching playlists
|
|
56
|
+
*/
|
|
57
|
+
async function fetchPlaylistsWithPagination(feedUrl, searchTerm, pageSize = 50, topN = 10, maxItems = 500) {
|
|
58
|
+
const allMatches = [];
|
|
59
|
+
let offset = 0;
|
|
60
|
+
let hasMore = true;
|
|
61
|
+
let totalFetched = 0;
|
|
62
|
+
while (hasMore && totalFetched < maxItems) {
|
|
63
|
+
// Calculate limit for this page (might be less than pageSize on last page)
|
|
64
|
+
// API has a maximum limit of 100
|
|
65
|
+
const remainingItems = maxItems - totalFetched;
|
|
66
|
+
const currentLimit = Math.min(pageSize, remainingItems, 100);
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(`${feedUrl}/playlists?limit=${currentLimit}&offset=${offset}&sort=-created`);
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
if (response.status === 404 || response.status === 400) {
|
|
71
|
+
// No more pages or offset not supported
|
|
72
|
+
hasMore = false;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
const data = await response.json();
|
|
78
|
+
const playlists = data.items || [];
|
|
79
|
+
if (playlists.length === 0) {
|
|
80
|
+
hasMore = false;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
totalFetched += playlists.length;
|
|
84
|
+
// Extract titles for fuzzy matching
|
|
85
|
+
const titles = playlists.map((p) => p.title);
|
|
86
|
+
// Fuzzy match on this page
|
|
87
|
+
const results = fuzzysort.go(searchTerm, titles, {
|
|
88
|
+
threshold: -5000, // More lenient threshold
|
|
89
|
+
limit: topN, // Keep only top N per page
|
|
90
|
+
});
|
|
91
|
+
// Map results back to playlist objects
|
|
92
|
+
results.forEach((result) => {
|
|
93
|
+
const playlist = playlists.find((p) => p.title === result.target);
|
|
94
|
+
if (playlist) {
|
|
95
|
+
allMatches.push({
|
|
96
|
+
title: playlist.title,
|
|
97
|
+
id: playlist.id,
|
|
98
|
+
feedUrl,
|
|
99
|
+
score: result.score,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
// Check if we've reached the end
|
|
104
|
+
if (playlists.length < currentLimit) {
|
|
105
|
+
hasMore = false;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
offset += playlists.length;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (_error) {
|
|
112
|
+
hasMore = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return allMatches;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Fetch all playlists from all configured feeds
|
|
119
|
+
*
|
|
120
|
+
* @returns {Promise<Array>} Array of all playlists from all feeds
|
|
121
|
+
*/
|
|
122
|
+
async function fetchAllPlaylists() {
|
|
123
|
+
const feedUrls = getFeedApiUrls();
|
|
124
|
+
// Fetch playlists from all feeds in parallel
|
|
125
|
+
const allPlaylistsArrays = await Promise.all(feedUrls.map((url) => fetchPlaylistsFromFeed(url)));
|
|
126
|
+
// Flatten and combine results from all feeds
|
|
127
|
+
return allPlaylistsArrays.flat();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Search for exact playlist match by name across multiple feeds
|
|
131
|
+
*
|
|
132
|
+
* @param {string} playlistName - Exact playlist name to search for
|
|
133
|
+
* @returns {Promise<Object>} Search result with playlist or error
|
|
134
|
+
*/
|
|
135
|
+
async function searchExactPlaylist(playlistName) {
|
|
136
|
+
try {
|
|
137
|
+
const playlists = await fetchAllPlaylists();
|
|
138
|
+
if (playlists.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
error: 'No playlists found in any feed',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Find exact match (case-insensitive)
|
|
145
|
+
const normalizedSearchName = playlistName.toLowerCase().trim();
|
|
146
|
+
const exactMatch = playlists.find((p) => p.title.toLowerCase().trim() === normalizedSearchName);
|
|
147
|
+
if (exactMatch) {
|
|
148
|
+
// Fetch full playlist details
|
|
149
|
+
const playlist = await getPlaylistById(exactMatch.id, exactMatch.feedUrl);
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
playlist,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
error: `No exact match found for playlist "${playlistName}"`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return {
|
|
164
|
+
success: false,
|
|
165
|
+
error: error.message,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Find the best matching playlist using fuzzy string matching with pagination
|
|
171
|
+
*
|
|
172
|
+
* @param {string} searchTerm - Search term to match against
|
|
173
|
+
* @returns {Promise<Object>} Result with best matching playlist name and map
|
|
174
|
+
*/
|
|
175
|
+
async function findBestMatchingPlaylist(searchTerm) {
|
|
176
|
+
try {
|
|
177
|
+
const feedUrls = getFeedApiUrls();
|
|
178
|
+
// Fetch from all feeds in parallel with pagination and filtering
|
|
179
|
+
const allMatchesArrays = await Promise.all(feedUrls.map((url) => fetchPlaylistsWithPagination(url, searchTerm, 50, 10)));
|
|
180
|
+
// Flatten and combine results from all feeds
|
|
181
|
+
const allMatches = allMatchesArrays.flat();
|
|
182
|
+
if (allMatches.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
error: `No matching playlists found for "${searchTerm}"`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// Sort by score (highest first) and get best match
|
|
189
|
+
allMatches.sort((a, b) => b.score - a.score);
|
|
190
|
+
// Build playlistMap for ID lookup
|
|
191
|
+
const playlistMap = {};
|
|
192
|
+
allMatches.forEach((match) => {
|
|
193
|
+
playlistMap[match.title] = {
|
|
194
|
+
id: match.id,
|
|
195
|
+
feedUrl: match.feedUrl,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
const bestMatch = allMatches[0].title;
|
|
199
|
+
// Simplified output: only show the best match, not all alternatives
|
|
200
|
+
console.log(chalk.green(`✓ Found: "${bestMatch}"`));
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
bestMatch,
|
|
204
|
+
playlistMap,
|
|
205
|
+
allMatches: allMatches.map((m) => m.title),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
error: error.message,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get playlist by ID or slug from a specific feed or try all feeds
|
|
217
|
+
*
|
|
218
|
+
* @param {string} idOrSlug - Playlist ID (UUID) or slug
|
|
219
|
+
* @param {string} [feedUrl] - Optional specific feed URL to use
|
|
220
|
+
* @returns {Promise<Object>} Playlist object
|
|
221
|
+
*/
|
|
222
|
+
async function getPlaylistById(idOrSlug, feedUrl = null) {
|
|
223
|
+
try {
|
|
224
|
+
const feedUrls = feedUrl ? [feedUrl] : getFeedApiUrls();
|
|
225
|
+
// Try each feed URL until we find the playlist
|
|
226
|
+
for (const url of feedUrls) {
|
|
227
|
+
try {
|
|
228
|
+
const response = await fetch(`${url}/playlists/${idOrSlug}`);
|
|
229
|
+
if (response.ok) {
|
|
230
|
+
const playlist = await response.json();
|
|
231
|
+
return playlist;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (_error) {
|
|
235
|
+
// Continue to next feed
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
throw new Error(`Playlist "${idOrSlug}" not found in any feed`);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
throw new Error(`Failed to fetch playlist: ${error.message}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Shuffle array using Fisher-Yates algorithm
|
|
247
|
+
*
|
|
248
|
+
* @param {Array} array - Array to shuffle
|
|
249
|
+
* @returns {Array} Shuffled array
|
|
250
|
+
*/
|
|
251
|
+
function shuffleArray(array) {
|
|
252
|
+
const shuffled = [...array];
|
|
253
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
254
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
255
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
256
|
+
}
|
|
257
|
+
return shuffled;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Get playlist items from a playlist
|
|
261
|
+
*
|
|
262
|
+
* @param {Object} playlist - DP1 playlist object
|
|
263
|
+
* @param {number} quantity - Number of items to extract
|
|
264
|
+
* @param {number} duration - Duration per item in seconds
|
|
265
|
+
* @param {boolean} shuffle - Whether to shuffle and randomly select items
|
|
266
|
+
* @returns {Array<Object>} Array of DP1 playlist items
|
|
267
|
+
*/
|
|
268
|
+
function extractPlaylistItems(playlist, quantity, duration, shuffle = true) {
|
|
269
|
+
if (!playlist.items || playlist.items.length === 0) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
let items = playlist.items;
|
|
273
|
+
// Shuffle and randomly select if requested
|
|
274
|
+
if (shuffle && items.length > quantity) {
|
|
275
|
+
items = shuffleArray(items);
|
|
276
|
+
}
|
|
277
|
+
// Take requested quantity
|
|
278
|
+
items = items.slice(0, quantity);
|
|
279
|
+
// Override duration and ensure created field exists
|
|
280
|
+
items = items.map((item) => ({
|
|
281
|
+
...item,
|
|
282
|
+
duration: duration || item.duration,
|
|
283
|
+
created: item.created || new Date().toISOString(), // Ensure created field exists
|
|
284
|
+
}));
|
|
285
|
+
return items;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Fetch feed playlist (deterministic - exact match only)
|
|
289
|
+
*
|
|
290
|
+
* @param {string} playlistName - Exact playlist name
|
|
291
|
+
* @param {number} quantity - Number of items to fetch
|
|
292
|
+
* @param {number} duration - Duration per item
|
|
293
|
+
* @returns {Promise<Object>} Result with items
|
|
294
|
+
*/
|
|
295
|
+
async function fetchFeedPlaylistDirect(playlistName, quantity = 5, duration = 10) {
|
|
296
|
+
const feedUrls = getFeedApiUrls();
|
|
297
|
+
console.log(chalk.cyan(`Searching for playlist "${playlistName}" in ${feedUrls.length} source(s)...`));
|
|
298
|
+
const result = await searchExactPlaylist(playlistName);
|
|
299
|
+
if (!result.success) {
|
|
300
|
+
console.log(chalk.yellow(` Playlist not found: ${result.error}`));
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
error: result.error,
|
|
304
|
+
items: [],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const items = extractPlaylistItems(result.playlist, quantity, duration);
|
|
308
|
+
console.log(chalk.green(`✓ Got ${items.length} item(s)\n`));
|
|
309
|
+
return {
|
|
310
|
+
success: true,
|
|
311
|
+
playlist: result.playlist,
|
|
312
|
+
items,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Search for playlists using fuzzy matching
|
|
317
|
+
*
|
|
318
|
+
* @param {string} playlistName - Playlist name (can be fuzzy)
|
|
319
|
+
* @param {number} quantity - Number of items to fetch
|
|
320
|
+
* @param {number} duration - Duration per item
|
|
321
|
+
* @returns {Promise<Object>} Result with best match and map for lookup
|
|
322
|
+
*/
|
|
323
|
+
async function searchFeedPlaylists(playlistName, quantity = 5, duration = 10) {
|
|
324
|
+
const feedUrls = getFeedApiUrls();
|
|
325
|
+
console.log(chalk.cyan(`Searching for playlist "${playlistName}" in ${feedUrls.length} source(s)...`));
|
|
326
|
+
const result = await findBestMatchingPlaylist(playlistName);
|
|
327
|
+
if (!result.success) {
|
|
328
|
+
console.log(chalk.yellow(` Playlist not found: ${result.error}\n`));
|
|
329
|
+
return {
|
|
330
|
+
success: false,
|
|
331
|
+
error: result.error,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
success: true,
|
|
336
|
+
bestMatch: result.bestMatch,
|
|
337
|
+
playlistMap: result.playlistMap,
|
|
338
|
+
searchTerm: playlistName,
|
|
339
|
+
quantity,
|
|
340
|
+
duration,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Fetch specific playlist by ID or name and extract items
|
|
345
|
+
*
|
|
346
|
+
* @param {string} playlistIdOrName - Playlist ID, slug, or exact name
|
|
347
|
+
* @param {number} quantity - Number of items to fetch
|
|
348
|
+
* @param {number} duration - Duration per item
|
|
349
|
+
* @param {Object} playlistMap - Optional map of names to IDs for lookup
|
|
350
|
+
* @param {boolean} shuffle - Whether to shuffle and randomly select items
|
|
351
|
+
* @returns {Promise<Object>} Result with items
|
|
352
|
+
*/
|
|
353
|
+
async function fetchPlaylistItems(playlistIdOrName, quantity = 5, duration = 10, playlistMap = null, shuffle = true) {
|
|
354
|
+
try {
|
|
355
|
+
let playlistId = playlistIdOrName;
|
|
356
|
+
let feedUrl = null;
|
|
357
|
+
// If playlistMap provided, look up the ID from the name
|
|
358
|
+
if (playlistMap && playlistMap[playlistIdOrName]) {
|
|
359
|
+
playlistId = playlistMap[playlistIdOrName].id;
|
|
360
|
+
feedUrl = playlistMap[playlistIdOrName].feedUrl;
|
|
361
|
+
}
|
|
362
|
+
const playlist = await getPlaylistById(playlistId, feedUrl);
|
|
363
|
+
const items = extractPlaylistItems(playlist, quantity, duration, shuffle);
|
|
364
|
+
console.log(chalk.green(`✓ Got ${items.length} item(s) from "${playlist.title || playlistId}"\n`));
|
|
365
|
+
return {
|
|
366
|
+
success: true,
|
|
367
|
+
playlist,
|
|
368
|
+
items,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
error: error.message,
|
|
375
|
+
items: [],
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
module.exports = {
|
|
380
|
+
searchExactPlaylist,
|
|
381
|
+
findBestMatchingPlaylist,
|
|
382
|
+
getPlaylistById,
|
|
383
|
+
extractPlaylistItems,
|
|
384
|
+
fetchFeedPlaylistDirect,
|
|
385
|
+
searchFeedPlaylists,
|
|
386
|
+
fetchPlaylistItems,
|
|
387
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FF1 Device Communication Module
|
|
4
|
+
* Handles sending DP1 playlists to FF1 devices via the Relayer API
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.sendPlaylistToDevice = sendPlaylistToDevice;
|
|
41
|
+
const config_1 = require("../config");
|
|
42
|
+
const logger = __importStar(require("../logger"));
|
|
43
|
+
/**
|
|
44
|
+
* Send a DP1 playlist to an FF1 device using the cast API
|
|
45
|
+
*
|
|
46
|
+
* This function sends the entire DP1 JSON payload to a configured FF1 device.
|
|
47
|
+
* If a device name is provided, it searches for a device with that exact name.
|
|
48
|
+
* If no device name is provided, it uses the first configured device.
|
|
49
|
+
* The API-KEY header is only included if the device has an apiKey configured.
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} params - Function parameters
|
|
52
|
+
* @param {Object} params.playlist - Complete DP1 v1.0.0 playlist object to send
|
|
53
|
+
* @param {string} [params.deviceName] - Name of the device to send to (exact match required)
|
|
54
|
+
* @returns {Promise<Object>} Result object
|
|
55
|
+
* @returns {boolean} returns.success - Whether the cast was successful
|
|
56
|
+
* @returns {string} [returns.device] - Device host that received the playlist
|
|
57
|
+
* @returns {string} [returns.deviceName] - Name of the device used
|
|
58
|
+
* @returns {Object} [returns.response] - Response from the device
|
|
59
|
+
* @returns {string} [returns.error] - Error message if failed
|
|
60
|
+
* @throws {Error} When device configuration is invalid or missing
|
|
61
|
+
* @example
|
|
62
|
+
* // Send to first device
|
|
63
|
+
* const result = await sendPlaylistToDevice({
|
|
64
|
+
* playlist: { version: '1.0.0', title: 'My Collection', items: [...] }
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Send to specific device by name
|
|
69
|
+
* const result = await sendPlaylistToDevice({
|
|
70
|
+
* playlist: { version: '1.0.0', title: 'My Collection', items: [...] },
|
|
71
|
+
* deviceName: 'Living Room Display'
|
|
72
|
+
* });
|
|
73
|
+
*/
|
|
74
|
+
async function sendPlaylistToDevice({ playlist, deviceName, }) {
|
|
75
|
+
try {
|
|
76
|
+
// Validate input
|
|
77
|
+
if (!playlist || typeof playlist !== 'object') {
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
error: 'Invalid playlist: must provide a valid DP1 playlist object',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Get device configuration
|
|
84
|
+
const deviceConfig = (0, config_1.getFF1DeviceConfig)();
|
|
85
|
+
if (!deviceConfig.devices || deviceConfig.devices.length === 0) {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
error: 'No FF1 devices configured. Please add devices to config.json under "ff1Devices"',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Find device by name if provided, otherwise use first device
|
|
92
|
+
let device;
|
|
93
|
+
if (deviceName) {
|
|
94
|
+
device = deviceConfig.devices.find((d) => d.name === deviceName);
|
|
95
|
+
if (!device) {
|
|
96
|
+
const availableNames = deviceConfig.devices
|
|
97
|
+
.map((d) => d.name)
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join(', ');
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
error: `Device "${deviceName}" not found. Available devices: ${availableNames || 'none with names'}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
logger.info(`Found device by name: ${deviceName}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
device = deviceConfig.devices[0];
|
|
109
|
+
logger.info('Using first configured device');
|
|
110
|
+
}
|
|
111
|
+
if (!device.host) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: 'Invalid device configuration: must include host',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
logger.info(`Sending playlist to FF1 device: ${device.host}`);
|
|
118
|
+
// Construct API URL with optional topicID
|
|
119
|
+
let apiUrl = `${device.host}/api/cast`;
|
|
120
|
+
if (device.topicID && device.topicID.trim() !== '') {
|
|
121
|
+
apiUrl += `?topicID=${encodeURIComponent(device.topicID)}`;
|
|
122
|
+
logger.debug(`Using topicID: ${device.topicID}`);
|
|
123
|
+
}
|
|
124
|
+
// Wrap playlist in required structure
|
|
125
|
+
const requestBody = {
|
|
126
|
+
command: 'displayPlaylist',
|
|
127
|
+
request: {
|
|
128
|
+
dp1_call: playlist,
|
|
129
|
+
intent: { action: 'now_display' },
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
// Prepare headers
|
|
133
|
+
const headers = {
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
};
|
|
136
|
+
// Add API-KEY header only if apiKey is provided
|
|
137
|
+
if (device.apiKey) {
|
|
138
|
+
headers['API-KEY'] = device.apiKey;
|
|
139
|
+
}
|
|
140
|
+
// Make the API request
|
|
141
|
+
const response = await fetch(apiUrl, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers,
|
|
144
|
+
body: JSON.stringify(requestBody),
|
|
145
|
+
});
|
|
146
|
+
// Check response status
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
const errorText = await response.text();
|
|
149
|
+
logger.error(`Failed to cast to device: ${response.status} ${response.statusText}`);
|
|
150
|
+
logger.debug(`Error details: ${errorText}`);
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
error: `Device returned error ${response.status}: ${response.statusText}`,
|
|
154
|
+
details: errorText,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// Parse response
|
|
158
|
+
const responseData = (await response.json());
|
|
159
|
+
logger.info('Successfully sent playlist to FF1 device');
|
|
160
|
+
logger.debug(`Device response: ${JSON.stringify(responseData)}`);
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
device: device.host,
|
|
164
|
+
deviceName: device.name || device.host,
|
|
165
|
+
response: responseData,
|
|
166
|
+
message: 'Playlist successfully sent to FF1 device',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
logger.error(`Error sending playlist to device: ${error.message}`);
|
|
171
|
+
return {
|
|
172
|
+
success: false,
|
|
173
|
+
error: error.message,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|