diff --git a/src/features/background/api/photoPackAPI.js b/src/features/background/api/photoPackAPI.js index 111ffddb..73ae8695 100644 --- a/src/features/background/api/photoPackAPI.js +++ b/src/features/background/api/photoPackAPI.js @@ -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} 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} 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) {