const { google } = require('googleapis'); const youtube = google.youtube('v3'); const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY; const PLAYLISTS = { seventies: 'PLmXxqSJJq-yXrCPGIT2gn8b34JjOrl4Xf', eighties: 'PLmXxqSJJq-yUvMWKuZQAB_8yxnjZaOZUp', nineties: 'PLmXxqSJJq-yUF3jbzjF_pa--kuBuMlyQQ', pop: 'PLxA687tYuMWhkqYjvAGtW_heiEL4Hk_Lx', dance: 'PL64E6BD94546734D8' }; let validatedPlaylists = {}; const VALIDATION_TTL = 3600000; // 1 hour const validatePlaylist = async (playlistId) => { try { const response = await youtube.playlists.list({ key: API_KEY, part: 'snippet,contentDetails', id: playlistId }); if (!response.data.items || response.data.items.length === 0) { console.log(`Playlist ${playlistId} not found or empty`); return false; } validatedPlaylists[playlistId] = { timestamp: Date.now(), valid: true }; return true; } catch (error) { console.error(`Failed to validate playlist ${playlistId}:`, error); return false; } }; const validateAndCleanPlaylists = async () => { const now = Date.now(); const validPlaylistIds = []; for (const [genre, playlistId] of Object.entries(PLAYLISTS)) { if (validatedPlaylists[playlistId] && (now - validatedPlaylists[playlistId].timestamp) < VALIDATION_TTL) { if (validatedPlaylists[playlistId].valid) { validPlaylistIds.push([genre, playlistId]); } continue; } const isValid = await validatePlaylist(playlistId); if (isValid) { validPlaylistIds.push([genre, playlistId]); } else { console.log(`Removing invalid playlist: ${genre} (${playlistId})`); delete PLAYLISTS[genre]; } } return validPlaylistIds; }; const API_KEY = process.env.YOUTUBE_API_KEY; const cachedSongsByPlaylist = {}; const CACHE_TTL = 3600000; // 1 hour /** * Fetches songs from YouTube playlist and returns them */ const fetchPlaylistSongs = async (playlistId = null) => { if (!playlistId) { console.warn("No playlist ID provided, using default"); playlistId = PLAYLISTS.eighties; } const now = Date.now(); if (cachedSongsByPlaylist[playlistId] && cachedSongsByPlaylist[playlistId].songs.length > 0 && (now - cachedSongsByPlaylist[playlistId].timestamp) < CACHE_TTL) { console.log(`Using cached songs for playlist ${playlistId}`); return cachedSongsByPlaylist[playlistId].songs; } try { console.log(`Fetching fresh songs from YouTube API for playlist ${playlistId}...`); const playlistUrl = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&maxResults=50&playlistId=${playlistId}&key=${YOUTUBE_API_KEY}`; const response = await fetch(playlistUrl); const data = await response.json(); if (data.error) { console.error("YouTube API error:", data.error); throw new Error(data.error.message); } if (!data.items || !data.items.length) { throw new Error("No songs found in the playlist"); } const songs = data.items.map((item, index) => ({ id: index + 1, youtubeId: item.contentDetails.videoId, title: item.snippet.title, artist: item.snippet.videoOwnerChannelTitle || "Unknown Artist", coverUrl: item.snippet.thumbnails.high?.url || item.snippet.thumbnails.default?.url, playlistId: playlistId })); cachedSongsByPlaylist[playlistId] = { songs, timestamp: now }; return songs; } catch (error) { console.error(`Error fetching YouTube playlist ${playlistId}:`, error); return cachedSongsByPlaylist[playlistId]?.songs || getDefaultSongs(); } }; const getAvailableSongIds = async (playlistId) => { const songs = await fetchPlaylistSongs(playlistId); return songs.map(song => song.id); }; async function getPlaylistDetails(availablePlaylists = null) { try { const playlistsToUse = availablePlaylists || await getRandomPlaylists(3); const details = {}; const validationPromises = []; for (const [genre, playlistId] of Object.entries(playlistsToUse)) { validationPromises.push( youtube.playlists.list({ key: API_KEY, part: 'snippet,contentDetails', id: playlistId }).then(response => { if (response.data.items?.[0]) { const playlist = response.data.items[0]; details[genre] = { id: playlistId, title: playlist.snippet.title, description: playlist.snippet.description, thumbnail: playlist.snippet.thumbnails.maxres || playlist.snippet.thumbnails.high, songCount: playlist.contentDetails.itemCount, votes: 0 }; } else { console.warn(`Playlist not found: ${genre} (${playlistId})`); } }).catch(error => { console.error(`Error fetching playlist ${playlistId}:`, error); }) ); } await Promise.all(validationPromises); if (Object.keys(details).length === 0) { const response = await youtube.playlists.list({ key: API_KEY, part: 'snippet,contentDetails', id: PLAYLISTS.seventies }); if (response.data.items?.[0]) { const playlist = response.data.items[0]; details.seventies = { id: PLAYLISTS.seventies, title: playlist.snippet.title, description: playlist.snippet.description, thumbnail: playlist.snippet.thumbnails.maxres || playlist.snippet.thumbnails.high, songCount: playlist.contentDetails.itemCount, votes: 0 }; } } if (Object.keys(details).length === 0) { throw new Error("No valid playlists found"); } return details; } catch (error) { console.error('Error fetching playlist details:', error); throw error; } } const getRandomPlaylists = async (count = 3) => { try { const allPlaylists = Object.entries(PLAYLISTS); if (allPlaylists.length === 0) { throw new Error("No playlists configured"); } const shuffled = [...allPlaylists].sort(() => Math.random() - 0.5); const selected = shuffled.slice(0, Math.min(count, allPlaylists.length)); const result = {}; for (const [genre, playlistId] of selected) { result[genre] = playlistId; } return result; } catch (error) { console.error("Error getting random playlists:", error); return { seventies: PLAYLISTS.seventies }; } }; module.exports = { fetchPlaylistSongs, getAvailableSongIds, PLAYLISTS, getPlaylistDetails, getRandomPlaylists, validateAndCleanPlaylists, validatePlaylist };