sthibert's picture
🐳 27/04 - 06:39 - have you updated or deployed this change on the live preview?
d34a4c6 verified
Raw
History Blame Contribute Delete
23.8 kB
// ===== 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_-]+)/,
/<meta\s+itemprop="channelId"\s+content="(UC[^"]+)"/,
/\" channelId\":\"(UC[^\"]+)\"/,
];
for (const pattern of patterns) {
const match = html.match(pattern);
if (match) {
console.log(`✅ Discovered channel ID: ${match[1]} via ${proxy}`);
return match[1];
}
}
} catch (e) {
console.warn(`Channel ID discovery via ${proxy} failed:`, e.message);
}
}
return null;
}
// ===== Fetch videos via rss2json.com (most reliable CORS-friendly method) =====
async function fetchViaRss2Json(channelId) {
try {
const rssUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
const url = `https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(rssUrl)}`;
const res = await fetchWithTimeout(url, 10000);
if (!res.ok) return null;
const data = await res.json();
if (data.status === 'ok' && data.items?.length) {
console.log(`✅ rss2json succeeded with ${data.items.length} videos`);
return data.items.slice(0, 10).map(item => {
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 `
<a href="https://www.youtube.com/watch?v=${video.videoId}" target="_blank" class="featured-card block group">
<div class="grid grid-cols-1 md:grid-cols-2 gap-0">
<div class="thumbnail-wrapper aspect-video md:aspect-auto">
<img src="${video.videoThumbnails?.find(t => t.quality === 'maxres')?.url || video.videoThumbnails?.[0]?.url || ''}"
alt="${video.title}"
class="w-full h-full object-cover"
onerror="this.src='http://static.photos/technology/640x360/1'">
<div class="play-overlay">
<div class="w-16 h-16 rounded-full bg-red-600/90 flex items-center justify-center shadow-lg shadow-red-600/30 group-hover:scale-110 transition-transform">
<svg class="w-7 h-7 text-white ml-1" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
</div>
</div>
${video.lengthSeconds ? `<span class="duration-badge">${formatDuration(video.lengthSeconds)}</span>` : ''}
</div>
<div class="p-6 md:p-8 flex flex-col justify-center">
<div class="flex items-center gap-2 mb-3">
<span class="px-2.5 py-0.5 rounded-full bg-brand-accent/10 border border-brand-accent/30 text-brand-accent text-xs font-medium">LATEST</span>
<span class="text-gray-500 text-xs">${formatDate(video.published)}</span>
</div>
<h3 class="font-display font-semibold text-lg sm:text-xl md:text-2xl mb-3 text-white group-hover:text-brand-accent transition-colors line-clamp-2">${video.title}</h3>
<p class="text-gray-500 text-sm line-clamp-2 mb-4">${video.description || 'AI-generated music by Binary Beats.'}</p>
<div class="flex items-center gap-4 text-xs text-gray-500">
${video.viewCount ? `<span class="flex items-center gap-1"><i data-lucide="eye" class="w-3.5 h-3.5"></i> ${formatViews(video.viewCount)}</span>` : ''}
${video.lengthSeconds ? `<span class="flex items-center gap-1"><i data-lucide="clock" class="w-3.5 h-3.5"></i> ${formatDuration(video.lengthSeconds)}</span>` : ''}
</div>
</div>
</div>
</a>
`;
}
function createVideoCard(video, index) {
const delay = index * 100;
return `
<a href="https://www.youtube.com/watch?v=${video.videoId}" target="_blank" class="video-card block group" style="animation-delay: ${delay}ms">
<div class="thumbnail-wrapper aspect-video">
<img src="${video.videoThumbnails?.find(t => t.quality === 'high')?.url || video.videoThumbnails?.[0]?.url || ''}"
alt="${video.title}"
class="w-full h-full object-cover"
onerror="this.src='http://static.photos/technology/320x240/${index + 2}'">
<div class="play-overlay">
<div class="w-12 h-12 rounded-full bg-red-600/90 flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform">
<svg class="w-5 h-5 text-white ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
</div>
</div>
${video.lengthSeconds ? `<span class="duration-badge">${formatDuration(video.lengthSeconds)}</span>` : ''}
</div>
<div class="p-3 sm:p-4">
<h3 class="font-semibold text-sm sm:text-base text-gray-200 group-hover:text-brand-accent transition-colors line-clamp-2 mb-2">${video.title}</h3>
<div class="flex items-center justify-between text-xs text-gray-500">
<span>${formatDate(video.published)}</span>
${video.viewCount ? `<span>${formatViews(video.viewCount)}</span>` : ''}
</div>
</div>
</a>
`;
}
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' });
}
});
});