@feralfile/cli 1.1.1
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 +96 -0
- package/config.json.example +96 -0
- package/dist/index.js +54 -0
- package/dist/src/ai-orchestrator/index.js +1019 -0
- package/dist/src/ai-orchestrator/registry.js +96 -0
- package/dist/src/commands/build.js +69 -0
- package/dist/src/commands/chat.js +189 -0
- package/dist/src/commands/config.js +68 -0
- package/dist/src/commands/device.js +278 -0
- package/dist/src/commands/helpers/config-files.js +62 -0
- package/dist/src/commands/helpers/device-discovery.js +111 -0
- package/dist/src/commands/helpers/playlist-display.js +161 -0
- package/dist/src/commands/helpers/prompt.js +65 -0
- package/dist/src/commands/helpers/ssh-helpers.js +44 -0
- package/dist/src/commands/play.js +110 -0
- package/dist/src/commands/publish.js +115 -0
- package/dist/src/commands/setup.js +225 -0
- package/dist/src/commands/sign.js +41 -0
- package/dist/src/commands/ssh.js +108 -0
- package/dist/src/commands/status.js +126 -0
- package/dist/src/commands/validate.js +18 -0
- package/dist/src/config.js +441 -0
- package/dist/src/intent-parser/index.js +1382 -0
- package/dist/src/intent-parser/utils.js +108 -0
- package/dist/src/logger.js +82 -0
- package/dist/src/main.js +459 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utilities/address-validator.js +242 -0
- package/dist/src/utilities/device-default.js +36 -0
- package/dist/src/utilities/device-lookup.js +107 -0
- package/dist/src/utilities/device-normalize.js +62 -0
- package/dist/src/utilities/device-upsert.js +91 -0
- package/dist/src/utilities/domain-resolver.js +291 -0
- package/dist/src/utilities/ed25519-key-derive.js +155 -0
- package/dist/src/utilities/feed-fetcher.js +471 -0
- package/dist/src/utilities/ff1-compatibility.js +269 -0
- package/dist/src/utilities/ff1-device.js +250 -0
- package/dist/src/utilities/ff1-discovery.js +330 -0
- package/dist/src/utilities/functions.js +308 -0
- package/dist/src/utilities/index.js +469 -0
- package/dist/src/utilities/nft-indexer.js +1024 -0
- package/dist/src/utilities/playlist-builder.js +523 -0
- package/dist/src/utilities/playlist-publisher.js +131 -0
- package/dist/src/utilities/playlist-send.js +260 -0
- package/dist/src/utilities/playlist-signer.js +204 -0
- package/dist/src/utilities/playlist-signing-role.js +41 -0
- package/dist/src/utilities/playlist-source.js +128 -0
- package/dist/src/utilities/playlist-verifier.js +274 -0
- package/dist/src/utilities/ssh-access.js +145 -0
- package/dist/src/utils.js +48 -0
- package/docs/CONFIGURATION.md +206 -0
- package/docs/EXAMPLES.md +390 -0
- package/docs/FUNCTION_CALLING.md +96 -0
- package/docs/PROJECT_SPEC.md +228 -0
- package/docs/README.md +348 -0
- package/docs/RELEASING.md +73 -0
- package/package.json +76 -0
|
@@ -0,0 +1,471 @@
|
|
|
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
|
+
* Build a concise feed reachability error message.
|
|
20
|
+
*
|
|
21
|
+
* @param {Array<string>} failedFeeds - Feed URLs that could not be reached
|
|
22
|
+
* @returns {string} Human-readable error message
|
|
23
|
+
*/
|
|
24
|
+
function buildFeedUnreachableError(failedFeeds = []) {
|
|
25
|
+
const failedCount = Array.isArray(failedFeeds) ? failedFeeds.length : 0;
|
|
26
|
+
if (failedCount === 0) {
|
|
27
|
+
return 'No reachable feed servers available';
|
|
28
|
+
}
|
|
29
|
+
if (failedCount === 1) {
|
|
30
|
+
return `Configured feed server is unreachable: ${failedFeeds[0]}`;
|
|
31
|
+
}
|
|
32
|
+
return `All configured feed servers are unreachable (${failedCount}): ${failedFeeds.join(', ')}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Fetch playlists from a single feed URL with pagination
|
|
36
|
+
*
|
|
37
|
+
* @param {string} feedUrl - Feed API base URL
|
|
38
|
+
* @param {number} limit - Items per page (default: 50, max: 100)
|
|
39
|
+
* @returns {Promise<Object>} Result with playlists and reachability
|
|
40
|
+
*/
|
|
41
|
+
async function fetchPlaylistsFromFeed(feedUrl, limit = 100) {
|
|
42
|
+
try {
|
|
43
|
+
// API has a maximum limit of 100
|
|
44
|
+
const validLimit = Math.min(limit, 100);
|
|
45
|
+
const response = await fetch(`${feedUrl}/playlists?limit=${validLimit}&sort=-created`);
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
console.log(chalk.yellow(` Feed ${feedUrl} returned ${response.status}`));
|
|
48
|
+
return {
|
|
49
|
+
playlists: [],
|
|
50
|
+
reachable: false,
|
|
51
|
+
feedUrl,
|
|
52
|
+
error: `HTTP ${response.status}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
const playlists = data.items || [];
|
|
57
|
+
// Add feedUrl to each playlist for tracking
|
|
58
|
+
return {
|
|
59
|
+
playlists: playlists.map((p) => ({
|
|
60
|
+
...p,
|
|
61
|
+
feedUrl,
|
|
62
|
+
})),
|
|
63
|
+
reachable: true,
|
|
64
|
+
feedUrl,
|
|
65
|
+
error: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.log(chalk.yellow(` Failed to fetch from ${feedUrl}: ${error.message}`));
|
|
70
|
+
return {
|
|
71
|
+
playlists: [],
|
|
72
|
+
reachable: false,
|
|
73
|
+
feedUrl,
|
|
74
|
+
error: error.message,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Fetch playlists with pagination and fuzzy filtering to save memory
|
|
80
|
+
*
|
|
81
|
+
* @param {string} feedUrl - Feed API base URL
|
|
82
|
+
* @param {string} searchTerm - Search term for fuzzy filtering
|
|
83
|
+
* @param {number} pageSize - Items per page (default: 50, max: 100)
|
|
84
|
+
* @param {number} topN - Keep top N matches per page (default: 10)
|
|
85
|
+
* @param {number} maxItems - Maximum total items to fetch (default: 500)
|
|
86
|
+
* @returns {Promise<Object>} Result with matches and reachability
|
|
87
|
+
*/
|
|
88
|
+
async function fetchPlaylistsWithPagination(feedUrl, searchTerm, pageSize = 50, topN = 10, maxItems = 500) {
|
|
89
|
+
const allMatches = [];
|
|
90
|
+
let reachable = false;
|
|
91
|
+
let offset = 0;
|
|
92
|
+
let hasMore = true;
|
|
93
|
+
let totalFetched = 0;
|
|
94
|
+
while (hasMore && totalFetched < maxItems) {
|
|
95
|
+
// Calculate limit for this page (might be less than pageSize on last page)
|
|
96
|
+
// API has a maximum limit of 100
|
|
97
|
+
const remainingItems = maxItems - totalFetched;
|
|
98
|
+
const currentLimit = Math.min(pageSize, remainingItems, 100);
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(`${feedUrl}/playlists?limit=${currentLimit}&offset=${offset}&sort=-created`);
|
|
101
|
+
reachable = true;
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
if (response.status === 404 || response.status === 400) {
|
|
104
|
+
// No more pages or offset not supported
|
|
105
|
+
hasMore = false;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
const data = await response.json();
|
|
111
|
+
const playlists = data.items || [];
|
|
112
|
+
if (playlists.length === 0) {
|
|
113
|
+
hasMore = false;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
totalFetched += playlists.length;
|
|
117
|
+
// Extract titles for fuzzy matching
|
|
118
|
+
const titles = playlists.map((p) => p.title);
|
|
119
|
+
// Fuzzy match on this page
|
|
120
|
+
const results = fuzzysort.go(searchTerm, titles, {
|
|
121
|
+
threshold: -5000, // More lenient threshold
|
|
122
|
+
limit: topN, // Keep only top N per page
|
|
123
|
+
});
|
|
124
|
+
// Map results back to playlist objects
|
|
125
|
+
results.forEach((result) => {
|
|
126
|
+
const playlist = playlists.find((p) => p.title === result.target);
|
|
127
|
+
if (playlist) {
|
|
128
|
+
allMatches.push({
|
|
129
|
+
title: playlist.title,
|
|
130
|
+
id: playlist.id,
|
|
131
|
+
feedUrl,
|
|
132
|
+
score: result.score,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// Check if we've reached the end
|
|
137
|
+
if (playlists.length < currentLimit) {
|
|
138
|
+
hasMore = false;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
offset += playlists.length;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (_error) {
|
|
145
|
+
hasMore = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
matches: allMatches,
|
|
150
|
+
reachable,
|
|
151
|
+
feedUrl,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Fetch all playlists from all configured feeds
|
|
156
|
+
*
|
|
157
|
+
* @returns {Promise<Object>} Playlists with feed reachability metadata
|
|
158
|
+
*/
|
|
159
|
+
async function fetchAllPlaylists() {
|
|
160
|
+
const feedUrls = getFeedApiUrls();
|
|
161
|
+
// Fetch playlists from all feeds in parallel
|
|
162
|
+
const feedResults = await Promise.all(feedUrls.map((url) => fetchPlaylistsFromFeed(url)));
|
|
163
|
+
const playlists = feedResults.flatMap((result) => result.playlists || []);
|
|
164
|
+
const reachableFeeds = feedResults
|
|
165
|
+
.filter((result) => result.reachable)
|
|
166
|
+
.map((result) => result.feedUrl);
|
|
167
|
+
const unreachableFeeds = feedResults
|
|
168
|
+
.filter((result) => !result.reachable)
|
|
169
|
+
.map((result) => result.feedUrl);
|
|
170
|
+
return {
|
|
171
|
+
playlists,
|
|
172
|
+
reachableFeeds,
|
|
173
|
+
unreachableFeeds,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Search for exact playlist match by name across multiple feeds
|
|
178
|
+
*
|
|
179
|
+
* @param {string} playlistName - Exact playlist name to search for
|
|
180
|
+
* @returns {Promise<Object>} Search result with playlist or error
|
|
181
|
+
*/
|
|
182
|
+
async function searchExactPlaylist(playlistName) {
|
|
183
|
+
try {
|
|
184
|
+
const { playlists, reachableFeeds, unreachableFeeds } = await fetchAllPlaylists();
|
|
185
|
+
if (reachableFeeds.length === 0) {
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
errorType: 'feed_unreachable',
|
|
189
|
+
error: buildFeedUnreachableError(unreachableFeeds),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (playlists.length === 0) {
|
|
193
|
+
return {
|
|
194
|
+
success: false,
|
|
195
|
+
errorType: 'playlist_not_found',
|
|
196
|
+
error: `No playlists available in reachable feed server(s) for "${playlistName}"`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// Find exact match (case-insensitive)
|
|
200
|
+
const normalizedSearchName = playlistName.toLowerCase().trim();
|
|
201
|
+
const exactMatch = playlists.find((p) => p.title.toLowerCase().trim() === normalizedSearchName);
|
|
202
|
+
if (exactMatch) {
|
|
203
|
+
// Fetch full playlist details
|
|
204
|
+
const playlist = await getPlaylistById(exactMatch.id, exactMatch.feedUrl);
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
playlist,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
errorType: 'playlist_not_found',
|
|
214
|
+
error: `No exact match found for playlist "${playlistName}"`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
success: false,
|
|
221
|
+
errorType: 'feed_unreachable',
|
|
222
|
+
error: error.message,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Find the best matching playlist using fuzzy string matching with pagination
|
|
228
|
+
*
|
|
229
|
+
* @param {string} searchTerm - Search term to match against
|
|
230
|
+
* @returns {Promise<Object>} Result with best matching playlist name and map
|
|
231
|
+
*/
|
|
232
|
+
async function findBestMatchingPlaylist(searchTerm) {
|
|
233
|
+
try {
|
|
234
|
+
const feedUrls = getFeedApiUrls();
|
|
235
|
+
// Fetch from all feeds in parallel with pagination and filtering
|
|
236
|
+
const allMatchesResults = await Promise.all(feedUrls.map((url) => fetchPlaylistsWithPagination(url, searchTerm, 50, 10)));
|
|
237
|
+
const reachableFeeds = allMatchesResults
|
|
238
|
+
.filter((result) => result.reachable)
|
|
239
|
+
.map((result) => result.feedUrl);
|
|
240
|
+
const unreachableFeeds = allMatchesResults
|
|
241
|
+
.filter((result) => !result.reachable)
|
|
242
|
+
.map((result) => result.feedUrl);
|
|
243
|
+
if (reachableFeeds.length === 0) {
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
errorType: 'feed_unreachable',
|
|
247
|
+
error: buildFeedUnreachableError(unreachableFeeds),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// Flatten and combine results from all feeds
|
|
251
|
+
const allMatches = allMatchesResults.flatMap((result) => result.matches || []);
|
|
252
|
+
if (allMatches.length === 0) {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
errorType: 'playlist_not_found',
|
|
256
|
+
error: `No matching playlists found for "${searchTerm}"`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// Sort by score (highest first) and get best match
|
|
260
|
+
allMatches.sort((a, b) => b.score - a.score);
|
|
261
|
+
// Build playlistMap for ID lookup
|
|
262
|
+
const playlistMap = {};
|
|
263
|
+
allMatches.forEach((match) => {
|
|
264
|
+
playlistMap[match.title] = {
|
|
265
|
+
id: match.id,
|
|
266
|
+
feedUrl: match.feedUrl,
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
const bestMatch = allMatches[0].title;
|
|
270
|
+
// Simplified output: only show the best match, not all alternatives
|
|
271
|
+
console.log(chalk.green(`✓ Found: "${bestMatch}"`));
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
bestMatch,
|
|
275
|
+
playlistMap,
|
|
276
|
+
allMatches: allMatches.map((m) => m.title),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
errorType: 'feed_unreachable',
|
|
283
|
+
error: error.message,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get playlist by ID or slug from a specific feed or try all feeds
|
|
289
|
+
*
|
|
290
|
+
* @param {string} idOrSlug - Playlist ID (UUID) or slug
|
|
291
|
+
* @param {string} [feedUrl] - Optional specific feed URL to use
|
|
292
|
+
* @returns {Promise<Object>} Playlist object
|
|
293
|
+
*/
|
|
294
|
+
async function getPlaylistById(idOrSlug, feedUrl = null) {
|
|
295
|
+
try {
|
|
296
|
+
const feedUrls = feedUrl ? [feedUrl] : getFeedApiUrls();
|
|
297
|
+
// Try each feed URL until we find the playlist
|
|
298
|
+
for (const url of feedUrls) {
|
|
299
|
+
try {
|
|
300
|
+
const response = await fetch(`${url}/playlists/${idOrSlug}`);
|
|
301
|
+
if (response.ok) {
|
|
302
|
+
const playlist = await response.json();
|
|
303
|
+
return playlist;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (_error) {
|
|
307
|
+
// Continue to next feed
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
throw new Error(`Playlist "${idOrSlug}" not found in any feed`);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
throw new Error(`Failed to fetch playlist: ${error.message}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Shuffle array using Fisher-Yates algorithm
|
|
319
|
+
*
|
|
320
|
+
* @param {Array} array - Array to shuffle
|
|
321
|
+
* @returns {Array} Shuffled array
|
|
322
|
+
*/
|
|
323
|
+
function shuffleArray(array) {
|
|
324
|
+
const shuffled = [...array];
|
|
325
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
326
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
327
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
328
|
+
}
|
|
329
|
+
return shuffled;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get playlist items from a playlist
|
|
333
|
+
*
|
|
334
|
+
* @param {Object} playlist - DP1 playlist object
|
|
335
|
+
* @param {number} quantity - Number of items to extract
|
|
336
|
+
* @param {number} duration - Duration per item in seconds
|
|
337
|
+
* @param {boolean} shuffle - Whether to shuffle and randomly select items
|
|
338
|
+
* @returns {Array<Object>} Array of DP1 playlist items
|
|
339
|
+
*/
|
|
340
|
+
function extractPlaylistItems(playlist, quantity, duration, shuffle = true) {
|
|
341
|
+
if (!playlist.items || playlist.items.length === 0) {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
let items = playlist.items;
|
|
345
|
+
// Shuffle and randomly select if requested
|
|
346
|
+
if (shuffle && items.length > quantity) {
|
|
347
|
+
items = shuffleArray(items);
|
|
348
|
+
}
|
|
349
|
+
// Take requested quantity
|
|
350
|
+
items = items.slice(0, quantity);
|
|
351
|
+
// Override duration and ensure created field exists
|
|
352
|
+
items = items.map((item) => ({
|
|
353
|
+
...item,
|
|
354
|
+
duration: duration || item.duration,
|
|
355
|
+
created: item.created || new Date().toISOString(), // Ensure created field exists
|
|
356
|
+
}));
|
|
357
|
+
return items;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Fetch feed playlist (deterministic - exact match only)
|
|
361
|
+
*
|
|
362
|
+
* @param {string} playlistName - Exact playlist name
|
|
363
|
+
* @param {number} quantity - Number of items to fetch
|
|
364
|
+
* @param {number} duration - Duration per item
|
|
365
|
+
* @returns {Promise<Object>} Result with items
|
|
366
|
+
*/
|
|
367
|
+
async function fetchFeedPlaylistDirect(playlistName, quantity = 5, duration = 10) {
|
|
368
|
+
const feedUrls = getFeedApiUrls();
|
|
369
|
+
console.log(chalk.cyan(`Searching for playlist "${playlistName}" in ${feedUrls.length} source(s)...`));
|
|
370
|
+
const result = await searchExactPlaylist(playlistName);
|
|
371
|
+
if (!result.success) {
|
|
372
|
+
if (result.errorType === 'feed_unreachable') {
|
|
373
|
+
console.log(chalk.yellow(` Feed server unavailable: ${result.error}`));
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
console.log(chalk.yellow(` Playlist not found: ${result.error}`));
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
success: false,
|
|
380
|
+
error: result.error,
|
|
381
|
+
errorType: result.errorType,
|
|
382
|
+
items: [],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const items = extractPlaylistItems(result.playlist, quantity, duration);
|
|
386
|
+
console.log(chalk.green(`✓ Got ${items.length} item(s)\n`));
|
|
387
|
+
return {
|
|
388
|
+
success: true,
|
|
389
|
+
playlist: result.playlist,
|
|
390
|
+
items,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Search for playlists using fuzzy matching
|
|
395
|
+
*
|
|
396
|
+
* @param {string} playlistName - Playlist name (can be fuzzy)
|
|
397
|
+
* @param {number} quantity - Number of items to fetch
|
|
398
|
+
* @param {number} duration - Duration per item
|
|
399
|
+
* @returns {Promise<Object>} Result with best match and map for lookup
|
|
400
|
+
*/
|
|
401
|
+
async function searchFeedPlaylists(playlistName, quantity = 5, duration = 10) {
|
|
402
|
+
const feedUrls = getFeedApiUrls();
|
|
403
|
+
console.log(chalk.cyan(`Searching for playlist "${playlistName}" in ${feedUrls.length} source(s)...`));
|
|
404
|
+
const result = await findBestMatchingPlaylist(playlistName);
|
|
405
|
+
if (!result.success) {
|
|
406
|
+
if (result.errorType === 'feed_unreachable') {
|
|
407
|
+
console.log(chalk.yellow(` Feed server unavailable: ${result.error}\n`));
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
console.log(chalk.yellow(` Playlist not found: ${result.error}\n`));
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
success: false,
|
|
414
|
+
error: result.error,
|
|
415
|
+
errorType: result.errorType,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
success: true,
|
|
420
|
+
bestMatch: result.bestMatch,
|
|
421
|
+
playlistMap: result.playlistMap,
|
|
422
|
+
searchTerm: playlistName,
|
|
423
|
+
quantity,
|
|
424
|
+
duration,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Fetch specific playlist by ID or name and extract items
|
|
429
|
+
*
|
|
430
|
+
* @param {string} playlistIdOrName - Playlist ID, slug, or exact name
|
|
431
|
+
* @param {number} quantity - Number of items to fetch
|
|
432
|
+
* @param {number} duration - Duration per item
|
|
433
|
+
* @param {Object} playlistMap - Optional map of names to IDs for lookup
|
|
434
|
+
* @param {boolean} shuffle - Whether to shuffle and randomly select items
|
|
435
|
+
* @returns {Promise<Object>} Result with items
|
|
436
|
+
*/
|
|
437
|
+
async function fetchPlaylistItems(playlistIdOrName, quantity = 5, duration = 10, playlistMap = null, shuffle = true) {
|
|
438
|
+
try {
|
|
439
|
+
let playlistId = playlistIdOrName;
|
|
440
|
+
let feedUrl = null;
|
|
441
|
+
// If playlistMap provided, look up the ID from the name
|
|
442
|
+
if (playlistMap && playlistMap[playlistIdOrName]) {
|
|
443
|
+
playlistId = playlistMap[playlistIdOrName].id;
|
|
444
|
+
feedUrl = playlistMap[playlistIdOrName].feedUrl;
|
|
445
|
+
}
|
|
446
|
+
const playlist = await getPlaylistById(playlistId, feedUrl);
|
|
447
|
+
const items = extractPlaylistItems(playlist, quantity, duration, shuffle);
|
|
448
|
+
console.log(chalk.green(`✓ Got ${items.length} item(s) from "${playlist.title || playlistId}"\n`));
|
|
449
|
+
return {
|
|
450
|
+
success: true,
|
|
451
|
+
playlist,
|
|
452
|
+
items,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
return {
|
|
457
|
+
success: false,
|
|
458
|
+
error: error.message,
|
|
459
|
+
items: [],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
module.exports = {
|
|
464
|
+
searchExactPlaylist,
|
|
465
|
+
findBestMatchingPlaylist,
|
|
466
|
+
getPlaylistById,
|
|
467
|
+
extractPlaylistItems,
|
|
468
|
+
fetchFeedPlaylistDirect,
|
|
469
|
+
searchFeedPlaylists,
|
|
470
|
+
fetchPlaylistItems,
|
|
471
|
+
};
|