mirror of
https://github.com/mue/mue.git
synced 2026-07-16 13:34:03 +02:00
feat(photoPackAPI): refactor photo fetching logic to support multiple providers and deprecate legacy functions
This commit is contained in:
@@ -1,85 +1,207 @@
|
||||
import variables from 'config/variables';
|
||||
|
||||
// Provider-specific response parsers
|
||||
const responseParser = {
|
||||
pexels: (data, query) => {
|
||||
const photos = data.photos || [];
|
||||
if (photos.length === 0) return null;
|
||||
const photo = photos[Math.floor(Math.random() * photos.length)];
|
||||
return {
|
||||
photographer: photo.photographer,
|
||||
location: photo.alt || query.get('query') || 'Unknown',
|
||||
url: { default: photo.src.original },
|
||||
blur_hash: null,
|
||||
colour: photo.avg_color,
|
||||
photo_page: photo.url,
|
||||
photographer_page: photo.photographer_url,
|
||||
};
|
||||
},
|
||||
|
||||
pixabay: (data, query) => {
|
||||
const hits = data.hits || [];
|
||||
if (hits.length === 0) return null;
|
||||
const photo = hits[Math.floor(Math.random() * hits.length)];
|
||||
return {
|
||||
photographer: photo.user,
|
||||
location: photo.tags || query.get('q') || 'Unknown',
|
||||
url: { default: photo.largeImageURL || photo.webformatURL },
|
||||
blur_hash: null,
|
||||
colour: null,
|
||||
photo_page: photo.pageURL,
|
||||
photographer_page: `https://pixabay.com/users/${photo.user}-${photo.user_id}/`,
|
||||
};
|
||||
},
|
||||
|
||||
flickr: async (data, query, apiKey) => {
|
||||
const photos = data.photos?.photo || [];
|
||||
if (photos.length === 0) return null;
|
||||
const photo = photos[Math.floor(Math.random() * photos.length)];
|
||||
|
||||
// Get photo details
|
||||
const infoParams = new URLSearchParams({
|
||||
method: 'flickr.photos.getInfo',
|
||||
api_key: apiKey,
|
||||
photo_id: photo.id,
|
||||
format: 'json',
|
||||
nojsoncallback: '1',
|
||||
});
|
||||
const infoResp = await fetch(`https://api.flickr.com/services/rest/?${infoParams}`);
|
||||
const info = await infoResp.json();
|
||||
|
||||
return {
|
||||
photographer: info.photo?.owner?.realname || info.photo?.owner?.username || 'Unknown',
|
||||
location: info.photo?.title?._content || photo.title || 'Unknown',
|
||||
url: { default: `https://live.staticflickr.com/${photo.server}/${photo.id}_${photo.secret}_b.jpg` },
|
||||
blur_hash: null,
|
||||
colour: null,
|
||||
photo_page: `https://www.flickr.com/photos/${photo.owner}/${photo.id}/`,
|
||||
photographer_page: `https://www.flickr.com/photos/${photo.owner}/`,
|
||||
};
|
||||
},
|
||||
|
||||
// Default parser for Mue backend (already returns correct format)
|
||||
default: (data) => ({
|
||||
photographer: data.photographer,
|
||||
location: data.location?.name || data.location || 'Unknown',
|
||||
url: { default: data.file },
|
||||
blur_hash: data.blur_hash || null,
|
||||
colour: data.colour,
|
||||
...(data.photo_page && { photo_page: data.photo_page }),
|
||||
...(data.photographer_page && { photographer_page: data.photographer_page }),
|
||||
...(data.category && { category: data.category }),
|
||||
...(data.camera && { camera: data.camera }),
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch photos from MUE API based on pack settings
|
||||
* Generic function to fetch photos from any API provider
|
||||
* Supports both direct API calls and backend proxy
|
||||
* @param {string} packId - The ID of the photo pack
|
||||
* @param {object} pack - The pack configuration with api_endpoint
|
||||
* @param {object} settings - Pack-specific settings
|
||||
* @returns {Promise<object|null>} Photo object or null on error
|
||||
*/
|
||||
export async function fetchFromMUE(packId, settings) {
|
||||
const { quality, categories } = settings;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
quality: quality || 'high',
|
||||
categories: (categories || []).join(','),
|
||||
});
|
||||
export async function fetchFromProvider(packId, pack, settings) {
|
||||
if (!pack.api_endpoint) {
|
||||
console.error(`Pack ${packId} has no api_endpoint defined`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${variables.constants.API_URL}/images/random?${params}`);
|
||||
const params = new URLSearchParams();
|
||||
const headers = {};
|
||||
let apiKey = null;
|
||||
|
||||
if (!response.ok) throw new Error(`API error: ${response.status}`);
|
||||
// Build query params and headers based on provider
|
||||
if (pack.direct_api) {
|
||||
// Direct API calls - provider-specific logic
|
||||
switch (pack.api_provider) {
|
||||
case 'pexels':
|
||||
apiKey = settings[`photoPack_${packId}_api_key`];
|
||||
if (apiKey) headers['Authorization'] = atob(apiKey);
|
||||
params.append('query', settings[`photoPack_${packId}_query`] || 'nature');
|
||||
params.append('orientation', settings[`photoPack_${packId}_orientation`] || 'landscape');
|
||||
params.append('per_page', '80');
|
||||
params.append('page', Math.floor(Math.random() * 5) + 1);
|
||||
break;
|
||||
|
||||
case 'pixabay':
|
||||
apiKey = settings[`photoPack_${packId}_api_key`];
|
||||
if (apiKey) params.append('key', atob(apiKey));
|
||||
params.append('q', settings[`photoPack_${packId}_query`] || 'nature');
|
||||
if (settings[`photoPack_${packId}_category`]) {
|
||||
params.append('category', settings[`photoPack_${packId}_category`]);
|
||||
}
|
||||
params.append('image_type', 'photo');
|
||||
params.append('orientation', 'horizontal');
|
||||
params.append('safesearch', 'true');
|
||||
params.append('per_page', '200');
|
||||
params.append('page', Math.floor(Math.random() * 3) + 1);
|
||||
break;
|
||||
|
||||
case 'flickr':
|
||||
apiKey = settings[`photoPack_${packId}_api_key`];
|
||||
if (apiKey) params.append('api_key', atob(apiKey));
|
||||
params.append('method', 'flickr.photos.search');
|
||||
params.append('tags', settings[`photoPack_${packId}_tags`] || 'landscape');
|
||||
const license = settings[`photoPack_${packId}_license`] || 'all';
|
||||
const licenseMap = {
|
||||
'all': '1,2,3,4,5,6,7,8,9,10',
|
||||
'cc_by': '4',
|
||||
'cc_by_sa': '5',
|
||||
'cc_by_nc': '2',
|
||||
'cc_by_nd': '6',
|
||||
'cc0': '9,10',
|
||||
};
|
||||
params.append('license', licenseMap[license] || licenseMap['all']);
|
||||
params.append('sort', 'interestingness-desc');
|
||||
params.append('content_type', '1');
|
||||
params.append('media', 'photos');
|
||||
params.append('per_page', '500');
|
||||
params.append('page', Math.floor(Math.random() * 3) + 1);
|
||||
params.append('format', 'json');
|
||||
params.append('nojsoncallback', '1');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Backend proxy - send settings as params (backend handles the rest)
|
||||
pack.settings_schema?.forEach((setting) => {
|
||||
let value = settings[`photoPack_${packId}_${setting.id}`];
|
||||
if (setting.secure && value) {
|
||||
value = atob(value);
|
||||
}
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
params.append(setting.id, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${pack.api_endpoint}?${params}`;
|
||||
const response = await fetch(url, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 429) {
|
||||
console.warn(`${pack.api_provider} rate limit hit`);
|
||||
}
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
photographer: data.photographer,
|
||||
location: data.location?.name || 'Unknown',
|
||||
url: { default: data.file },
|
||||
blur_hash: data.blur_hash,
|
||||
colour: data.colour,
|
||||
category: data.category,
|
||||
camera: data.camera,
|
||||
};
|
||||
// Parse response using provider-specific parser
|
||||
const parser = responseParser[pack.api_provider] || responseParser.default;
|
||||
return await parser(data, params, apiKey ? atob(apiKey) : null);
|
||||
|
||||
} catch (error) {
|
||||
console.error('MUE API fetch failed:', error);
|
||||
console.error(`Fetch from ${pack.api_provider} failed:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch photos from Unsplash API (via Mue API proxy)
|
||||
* @param {string} packId - The ID of the photo pack
|
||||
* @param {object} settings - Pack-specific settings
|
||||
* @returns {Promise<object|null>} Photo object or null on error
|
||||
* @deprecated Legacy function - use fetchFromProvider instead
|
||||
* Kept for backwards compatibility
|
||||
*/
|
||||
export async function fetchFromMUE(packId, settings) {
|
||||
const pack = {
|
||||
api_endpoint: `${variables.constants.API_URL}/images/random`,
|
||||
api_provider: 'mue',
|
||||
settings_schema: [],
|
||||
};
|
||||
return fetchFromProvider(packId, pack, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Legacy function - use fetchFromProvider instead
|
||||
* Kept for backwards compatibility
|
||||
*/
|
||||
export async function fetchFromUnsplash(packId, settings) {
|
||||
const { collections } = settings;
|
||||
const trimmedCollections = collections?.trim();
|
||||
|
||||
const params = new URLSearchParams({
|
||||
quality: 'high',
|
||||
});
|
||||
|
||||
if (trimmedCollections) {
|
||||
params.append('collections', trimmedCollections);
|
||||
}
|
||||
|
||||
try {
|
||||
// Use Mue API proxy which has the Unsplash token server-side
|
||||
const response = await fetch(`${variables.constants.API_URL}/images/unsplash?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 429) {
|
||||
console.warn('Unsplash rate limit hit');
|
||||
}
|
||||
throw new Error(`Unsplash API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
photographer: data.photographer,
|
||||
location: data.location?.name || 'Unknown',
|
||||
url: { default: data.file },
|
||||
blur_hash: data.blur_hash,
|
||||
colour: data.colour,
|
||||
unsplash_url: data.photo_page,
|
||||
photographer_url: data.photographer_page,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Unsplash API fetch failed:', error);
|
||||
return null;
|
||||
}
|
||||
const pack = {
|
||||
api_endpoint: `${variables.constants.API_URL}/images/unsplash`,
|
||||
api_provider: 'unsplash',
|
||||
settings_schema: [],
|
||||
};
|
||||
return fetchFromProvider(packId, pack, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,10 +217,8 @@ export async function refreshAPIPackCache(packId) {
|
||||
|
||||
const settings = JSON.parse(localStorage.getItem(`photopack_settings_${packId}`) || '{}');
|
||||
|
||||
const fetchFunction = pack.api_provider === 'mue' ? fetchFromMUE : fetchFromUnsplash;
|
||||
|
||||
// Fetch 8 photos
|
||||
const promises = Array.from({ length: 8 }, () => fetchFunction(packId, settings));
|
||||
// Use generic provider function that works for any API
|
||||
const promises = Array.from({ length: 8 }, () => fetchFromProvider(packId, pack, settings));
|
||||
const results = await Promise.all(promises);
|
||||
const validPhotos = results.filter(Boolean);
|
||||
|
||||
@@ -130,21 +250,28 @@ export async function refreshAPIPackCache(packId) {
|
||||
|
||||
/**
|
||||
* Check all API packs and refresh if needed
|
||||
* Hardcoded 1 hour refresh interval (separate from global background change frequency)
|
||||
* Supports per-pack cache_refresh_interval (default: 1 hour)
|
||||
*/
|
||||
export async function checkAndRefreshAPIPacks() {
|
||||
const apiPacksReady = JSON.parse(localStorage.getItem('api_packs_ready') || '[]');
|
||||
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
|
||||
const installed = JSON.parse(localStorage.getItem('installed') || '[]');
|
||||
|
||||
// Hardcoded 1 hour refresh interval (separate from global background change frequency)
|
||||
const CACHE_REFRESH_INTERVAL = 3600 * 1000; // 1 hour in milliseconds
|
||||
// Default 1 hour refresh interval
|
||||
const DEFAULT_CACHE_REFRESH_INTERVAL = 3600 * 1000; // 1 hour in milliseconds
|
||||
|
||||
for (const packId of apiPacksReady) {
|
||||
const pack = installed.find((p) => p.id === packId);
|
||||
const cached = apiPackCache[packId];
|
||||
|
||||
// Use pack-specific interval or default
|
||||
const refreshInterval = pack?.cache_refresh_interval
|
||||
? pack.cache_refresh_interval * 1000
|
||||
: DEFAULT_CACHE_REFRESH_INTERVAL;
|
||||
|
||||
const needsRefresh =
|
||||
!cached ||
|
||||
Date.now() - cached.last_fetched > CACHE_REFRESH_INTERVAL ||
|
||||
Date.now() - cached.last_fetched > refreshInterval ||
|
||||
cached.photos.length < 3;
|
||||
|
||||
if (needsRefresh) {
|
||||
|
||||
Reference in New Issue
Block a user