// ===== Initialize Lucide Icons ===== lucide.createIcons(); // ===== Navbar Scroll Effect ===== const navbar = document.getElementById('navbar'); window.addEventListener('scroll', () => { if (window.scrollY > 50) { navbar.classList.add('scrolled'); } else { navbar.classList.remove('scrolled'); } }); // ===== Mobile Menu Toggle ===== const mobileMenuBtn = document.getElementById('mobileMenuBtn'); const mobileMenu = document.getElementById('mobileMenu'); let menuOpen = false; mobileMenuBtn.addEventListener('click', () => { menuOpen = !menuOpen; mobileMenu.classList.toggle('hidden', !menuOpen); }); // Close mobile menu when clicking a link document.querySelectorAll('.mobile-nav-link').forEach(link => { link.addEventListener('click', () => { menuOpen = false; mobileMenu.classList.add('hidden'); }); }); // ===== Section Reveal Animation ===== const revealElements = document.querySelectorAll('.reveal'); const revealObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } }); }, { threshold: 0.1 }); revealElements.forEach(el => revealObserver.observe(el)); // ===== Fetch YouTube Videos ===== const CHANNEL_HANDLE = 'binarybeats_ai'; const loadingState = document.getElementById('loadingState'); const errorState = document.getElementById('errorState'); const featuredVideo = document.getElementById('featuredVideo'); const videoGridRest = document.getElementById('videoGridRest'); const PIPED_INSTANCES = [ 'https://pipedapi.kavin.rocks', 'https://pipedapi.adminforge.de', 'https://pipedapi.in.projectsegfau.lt', 'https://pipedapi.r4fo.com', 'https://pipedapi.darkness.services', ]; const INVIDIOUS_INSTANCES = [ 'https://vid.puffyan.us', 'https://inv.nadeko.net', 'https://invidious.nerdvpn.de', 'https://iv.ggtyler.dev', 'https://invidious.privacyredirect.com', 'https://yewtu.be', ]; const CORS_PROXIES = [ 'https://api.allorigins.win/raw?url=', 'https://corsproxy.io/?', 'https://api.codetabs.com/v1/proxy?quest=', ]; async function fetchWithTimeout(url, timeout = 10000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(id); return response; } catch (e) { clearTimeout(id); throw e; } } function normalizePipedStream(stream) { return { videoId: stream.url?.replace('/watch?v=', '') || '', title: stream.title || 'Untitled', description: '', videoThumbnails: stream.thumbnail ? [ { url: stream.thumbnail, quality: 'maxres' }, { url: stream.thumbnail, quality: 'high' }, ] : [], published: stream.uploaded ? Math.floor(stream.uploaded / 1000) : null, lengthSeconds: stream.duration || 0, viewCount: stream.views || 0, }; } function normalizeInvidiousVideo(video) { return { videoId: video.videoId || '', title: video.title || 'Untitled', description: video.description || '', videoThumbnails: video.videoThumbnails || [], published: video.published || null, lengthSeconds: video.lengthSeconds || 0, viewCount: video.viewCount || 0, }; } // ===== Discover YouTube Channel ID from page HTML ===== async function discoverChannelId() { for (const proxy of CORS_PROXIES) { try { const ytUrl = encodeURIComponent(`https://www.youtube.com/@${CHANNEL_HANDLE}`); const res = await fetchWithTimeout(`${proxy}${ytUrl}`, 12000); if (!res.ok) continue; const html = await res.text(); // Try multiple patterns to extract channel ID from YouTube page const patterns = [ /\"channelId\":\"(UC[^\"]+)\"/, /channel_id=(UC[a-zA-Z0-9_-]+)/, /\"externalId\":\"(UC[^\"]+)\"/, /youtube\.com\/channel\/(UC[a-zA-Z0-9_-]+)/, / { const videoId = item.link?.match(/[?&]v=([^&]+)/)?.[1] || item.guid?.replace('yt:video:', '') || ''; const thumbnail = item.thumbnail || `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; return { videoId, title: item.title || 'Untitled', description: '', videoThumbnails: [ { url: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`, quality: 'maxres' }, { url: `https://i.ytimg.com/vi/${videoId}/sddefault.jpg`, quality: 'high' }, { url: thumbnail, quality: 'medium' }, ], published: item.pubDate ? Math.floor(new Date(item.pubDate).getTime() / 1000) : null, lengthSeconds: 0, viewCount: 0, }; }); } } catch (e) { console.warn('rss2json failed:', e.message); } return null; } async function fetchChannelVideos() { // Method 1: Discover channel ID, then use rss2json.com (most reliable CORS-friendly approach) const channelId = await discoverChannelId(); if (channelId) { const videos = await fetchViaRss2Json(channelId); if (videos) return videos; } // Method 2: Piped API directly (supports channel handles) for (const instance of PIPED_INSTANCES) { try { const res = await fetchWithTimeout(`${instance}/c/${CHANNEL_HANDLE}`); if (!res.ok) continue; const data = await res.json(); if (data.relatedStreams?.length) { console.log(`✅ Piped direct succeeded: ${instance}`); return data.relatedStreams.slice(0, 10).map(normalizePipedStream); } } catch (e) { console.warn(`Piped direct ${instance} failed:`, e.message); } } // Method 3: Piped API via CORS proxy for (const instance of PIPED_INSTANCES) { for (const proxy of CORS_PROXIES) { try { const encodedUrl = encodeURIComponent(`${instance}/c/${CHANNEL_HANDLE}`); const res = await fetchWithTimeout(`${proxy}${encodedUrl}`); if (!res.ok) continue; const data = await res.json(); if (data.relatedStreams?.length) { console.log(`✅ Piped proxy succeeded: ${proxy} → ${instance}`); return data.relatedStreams.slice(0, 10).map(normalizePipedStream); } } catch (e) { console.warn(`Piped proxy ${proxy}→${instance} failed:`, e.message); } } } // Method 4: Invidious API directly for (const instance of INVIDIOUS_INSTANCES) { try { const res = await fetchWithTimeout(`${instance}/api/v1/channels/${CHANNEL_HANDLE}`); if (res.ok) { const data = await res.json(); if (data?.authorId) { const videosRes = await fetchWithTimeout(`${instance}/api/v1/channels/${data.authorId}/videos?sort_by=newest`); if (videosRes.ok) { const videosData = await videosRes.json(); const videos = videosData.videos || videosData; if (videos?.length) { console.log(`✅ Invidious direct succeeded: ${instance}`); return videos.slice(0, 10).map(normalizeInvidiousVideo); } } } } // Fallback: search for the channel const searchRes = await fetchWithTimeout(`${instance}/api/v1/search?q=${CHANNEL_HANDLE}&type=channel`); if (!searchRes.ok) continue; const searchData = await searchRes.json(); const channel = searchData?.find(r => r.type === 'channel'); if (!channel) continue; const videosRes = await fetchWithTimeout(`${instance}/api/v1/channels/${channel.authorId}/videos?sort_by=newest`); if (!videosRes.ok) continue; const videosData = await videosRes.json(); const videos = videosData.videos || videosData; if (videos?.length) { console.log(`✅ Invidious search succeeded: ${instance}`); return videos.slice(0, 10).map(normalizeInvidiousVideo); } } catch (e) { console.warn(`Invidious direct ${instance} failed:`, e.message); } } // Method 5: Invidious API via CORS proxy for (const instance of INVIDIOUS_INSTANCES) { for (const proxy of CORS_PROXIES) { try { const encodedUrl = encodeURIComponent(`${instance}/api/v1/channels/${CHANNEL_HANDLE}`); const res = await fetchWithTimeout(`${proxy}${encodedUrl}`); if (!res.ok) continue; const data = await res.json(); if (data?.authorId) { const vidEncodedUrl = encodeURIComponent(`${instance}/api/v1/channels/${data.authorId}/videos?sort_by=newest`); const videosRes = await fetchWithTimeout(`${proxy}${vidEncodedUrl}`); if (!videosRes.ok) continue; const videosData = await videosRes.json(); const videos = videosData.videos || videosData; if (videos?.length) { console.log(`✅ Invidious proxy succeeded: ${proxy}→${instance}`); return videos.slice(0, 10).map(normalizeInvidiousVideo); } } } catch (e) { console.warn(`Invidious proxy ${proxy}→${instance} failed:`, e.message); } } } // Method 6: YouTube RSS feed via CORS proxy (direct XML parsing) if (channelId) { for (const proxy of CORS_PROXIES) { try { const rssUrl = encodeURIComponent(`https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`); const rssRes = await fetchWithTimeout(`${proxy}${rssUrl}`); if (!rssRes.ok) continue; const rssText = await rssRes.text(); const parser = new DOMParser(); const doc = parser.parseFromString(rssText, 'text/xml'); const entries = doc.querySelectorAll('entry'); if (entries.length) { console.log(`✅ YouTube RSS direct succeeded: ${proxy}`); return Array.from(entries).slice(0, 10).map(entry => { const videoId = entry.querySelector('videoId')?.textContent || entry.querySelector('link')?.getAttribute('href')?.match(/v=([^&]+)/)?.[1] || ''; const title = entry.querySelector('title')?.textContent || 'Untitled'; const publishedEl = entry.querySelector('published'); const published = publishedEl?.textContent ? Math.floor(new Date(publishedEl.textContent).getTime() / 1000) : null; const thumbnail = entry.querySelector('thumbnail')?.getAttribute('url') || entry.querySelectorNS('http://search.yahoo.com/mrss/', 'thumbnail')?.getAttribute('url') || `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; return { videoId, title, description: '', videoThumbnails: [ { url: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`, quality: 'maxres' }, { url: `https://i.ytimg.com/vi/${videoId}/sddefault.jpg`, quality: 'high' }, { url: thumbnail, quality: 'medium' }, ], published, lengthSeconds: 0, viewCount: 0, }; }); } } catch (e) { console.warn(`YouTube RSS direct ${proxy} failed:`, e.message); } } } return null; } function formatDate(timestamp) { if (!timestamp) return ''; const date = new Date(timestamp * 1000); const now = new Date(); const diff = now - date; const days = Math.floor(diff / (1000 * 60 * 60 * 24)); if (days === 0) return 'Today'; if (days === 1) return 'Yesterday'; if (days < 7) return `${days} days ago`; if (days < 30) return `${Math.floor(days / 7)} weeks ago`; if (days < 365) return `${Math.floor(days / 30)} months ago`; return `${Math.floor(days / 365)} years ago`; } function formatDuration(seconds) { if (!seconds) return ''; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; return `${m}:${s.toString().padStart(2, '0')}`; } function formatViews(views) { if (!views) return ''; if (views >= 1000000) return `${(views / 1000000).toFixed(1)}M views`; if (views >= 1000) return `${(views / 1000).toFixed(1)}K views`; return `${views} views`; } function createFeaturedCard(video) { return ` ${video.lengthSeconds ? `${formatDuration(video.lengthSeconds)}` : ''} LATEST ${formatDate(video.published)} ${video.title} ${video.description || 'AI-generated music by Binary Beats.'} ${video.viewCount ? ` ${formatViews(video.viewCount)}` : ''} ${video.lengthSeconds ? ` ${formatDuration(video.lengthSeconds)}` : ''} `; } function createVideoCard(video, index) { const delay = index * 100; return ` ${video.lengthSeconds ? `${formatDuration(video.lengthSeconds)}` : ''} ${video.title} ${formatDate(video.published)} ${video.viewCount ? `${formatViews(video.viewCount)}` : ''} `; } async function loadVideos() { const videos = await fetchChannelVideos(); if (!videos || videos.length === 0) { loadingState.classList.add('hidden'); errorState.classList.remove('hidden'); lucide.createIcons(); return; } const latestVideos = videos.slice(0, 10); // Show featured (latest) video featuredVideo.innerHTML = createFeaturedCard(latestVideos[0]); featuredVideo.classList.remove('hidden'); // Show rest in grid if (latestVideos.length > 1) { videoGridRest.innerHTML = latestVideos.slice(1).map((v, i) => createVideoCard(v, i + 1)).join(''); videoGridRest.classList.remove('hidden'); } // Update track count document.getElementById('trackCount').textContent = `${videos.length}+`; // Hide loading, show content loadingState.classList.add('hidden'); // Re-initialize lucide icons for dynamic content lucide.createIcons(); } // Start loading videos loadVideos(); // ===== Particle Background ===== const particlesCanvas = document.getElementById('particlesCanvas'); const pCtx = particlesCanvas.getContext('2d'); let particles = []; function resizeParticles() { particlesCanvas.width = window.innerWidth; particlesCanvas.height = window.innerHeight; } class Particle { constructor() { this.reset(); } reset() { this.x = Math.random() * particlesCanvas.width; this.y = Math.random() * particlesCanvas.height; this.size = Math.random() * 2 + 0.5; this.speedX = (Math.random() - 0.5) * 0.3; this.speedY = (Math.random() - 0.5) * 0.3; this.opacity = Math.random() * 0.5 + 0.1; this.color = Math.random() > 0.5 ? '0, 240, 255' : '168, 85, 247'; } update() { this.x += this.speedX; this.y += this.speedY; if (this.x < 0 || this.x > particlesCanvas.width || this.y < 0 || this.y > particlesCanvas.height) { this.reset(); } } draw() { pCtx.beginPath(); pCtx.arc(this.x, this.y, this.size, 0, Math.PI * 2); pCtx.fillStyle = `rgba(${this.color}, ${this.opacity})`; pCtx.fill(); } } function initParticles() { resizeParticles(); const count = Math.min(Math.floor((particlesCanvas.width * particlesCanvas.height) / 15000), 80); particles = []; for (let i = 0; i < count; i++) { particles.push(new Particle()); } } function animateParticles() { pCtx.clearRect(0, 0, particlesCanvas.width, particlesCanvas.height); particles.forEach(p => { p.update(); p.draw(); }); // Draw connections for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 120) { pCtx.beginPath(); pCtx.moveTo(particles[i].x, particles[i].y); pCtx.lineTo(particles[j].x, particles[j].y); pCtx.strokeStyle = `rgba(0, 240, 255, ${0.05 * (1 - dist / 120)})`; pCtx.lineWidth = 0.5; pCtx.stroke(); } } } requestAnimationFrame(animateParticles); } window.addEventListener('resize', () => { resizeParticles(); initParticles(); }); initParticles(); animateParticles(); // ===== Audio Visualizer ===== const vizCanvas = document.getElementById('visualizerCanvas'); const vCtx = vizCanvas.getContext('2d'); let mouseX = 0; let mouseY = 0; let time = 0; function resizeVisualizer() { const rect = vizCanvas.getBoundingClientRect(); vizCanvas.width = rect.width * window.devicePixelRatio; vizCanvas.height = rect.height * window.devicePixelRatio; vCtx.scale(window.devicePixelRatio, window.devicePixelRatio); } vizCanvas.addEventListener('mousemove', (e) => { const rect = vizCanvas.getBoundingClientRect(); mouseX = e.clientX - rect.left; mouseY = e.clientY - rect.top; }); vizCanvas.addEventListener('touchmove', (e) => { const rect = vizCanvas.getBoundingClientRect(); mouseX = e.touches[0].clientX - rect.left; mouseY = e.touches[0].clientY - rect.top; }); function drawVisualizer() { const w = vizCanvas.width / window.devicePixelRatio; const h = vizCanvas.height / window.devicePixelRatio; vCtx.clearRect(0, 0, w, h); const centerY = h / 2; const barCount = 80; const barWidth = w / barCount; const maxBarHeight = h * 0.8; for (let i = 0; i < barCount; i++) { const x = i * barWidth; const normalizedX = i / barCount; // Create wave pattern influenced by mouse const distFromMouse = Math.abs(normalizedX - (mouseX / w)); const mouseInfluence = Math.max(0, 1 - distFromMouse * 3); const wave1 = Math.sin(normalizedX * Math.PI * 4 + time * 0.03) * 0.3; const wave2 = Math.sin(normalizedX * Math.PI * 8 + time * 0.05) * 0.15; const wave3 = Math.cos(normalizedX * Math.PI * 2 + time * 0.02) * 0.2; const mouseWave = mouseInfluence * 0.4; const barHeight = Math.abs(wave1 + wave2 + wave3 + mouseWave) * maxBarHeight; const finalHeight = Math.max(2, barHeight); // Gradient for each bar const gradient = vCtx.createLinearGradient(x, centerY - finalHeight / 2, x, centerY + finalHeight / 2); gradient.addColorStop(0, `rgba(0, 240, 255, ${0.6 + mouseInfluence * 0.4})`); gradient.addColorStop(0.5, `rgba(168, 85, 247, ${0.4 + mouseInfluence * 0.4})`); gradient.addColorStop(1, `rgba(236, 72, 153, ${0.3 + mouseInfluence * 0.3})`); // Top bar (mirror) vCtx.fillStyle = gradient; vCtx.fillRect(x + 1, centerY - finalHeight / 2, barWidth - 2, finalHeight / 2); // Bottom bar (mirror reflection) const reflectionGradient = vCtx.createLinearGradient(x, centerY, x, centerY + finalHeight / 2); reflectionGradient.addColorStop(0, `rgba(0, 240, 255, ${0.2 + mouseInfluence * 0.2})`); reflectionGradient.addColorStop(1, 'rgba(0, 240, 255, 0)'); vCtx.fillStyle = reflectionGradient; vCtx.fillRect(x + 1, centerY, barWidth - 2, finalHeight / 2); // Glow dot on top if (mouseInfluence > 0.3) { vCtx.beginPath(); vCtx.arc(x + barWidth / 2, centerY - finalHeight / 2, 2 + mouseInfluence * 3, 0, Math.PI * 2); vCtx.fillStyle = `rgba(0, 240, 255, ${mouseInfluence * 0.8})`; vCtx.fill(); } } // Center line vCtx.beginPath(); vCtx.moveTo(0, centerY); vCtx.lineTo(w, centerY); vCtx.strokeStyle = 'rgba(0, 240, 255, 0.1)'; vCtx.lineWidth = 1; vCtx.stroke(); time++; requestAnimationFrame(drawVisualizer); } window.addEventListener('resize', resizeVisualizer); resizeVisualizer(); drawVisualizer(); // ===== Smooth Scroll for Anchor Links ===== document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); });
${video.description || 'AI-generated music by Binary Beats.'}