initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
'use server';
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '../../../lib/supabase/server';
const WP_BASE_URL = 'https://www.broadcastbeat.com/wp-json/wp/v2';
const PER_PAGE = 20;
const IMAGE_BUCKET = 'post-images';
interface WPPost {
id: number;
slug: string;
title: { rendered: string };
excerpt: { rendered: string };
content: { rendered: string };
date: string;
status: string;
_embedded?: {
author?: Array<{ name: string }>;
'wp:featuredmedia'?: Array<{ source_url: string; alt_text: string }>;
'wp:term'?: Array<Array<{ name: string; taxonomy: string }>>;
};
}
function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, '').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#039;/g, "'").replace(/&nbsp;/g, ' ').trim();
}
async function fetchWPPosts(page: number): Promise<{ posts: WPPost[]; totalPages: number }> {
const url = `${WP_BASE_URL}/posts?page=${page}&per_page=${PER_PAGE}&_embed=1&status=publish`;
const res = await fetch(url, {
headers: { 'User-Agent': 'BroadcastBeat-Importer/1.0' },
next: { revalidate: 0 },
});
if (!res.ok) {
throw new Error(`WordPress API error: ${res.status} ${res.statusText}`);
}
const totalPages = parseInt(res.headers.get('X-WP-TotalPages') || '1', 10);
const posts: WPPost[] = await res.json();
return { posts, totalPages };
}
/**
* Downloads an image from a remote URL and uploads it to Supabase Storage.
* Returns the public URL of the stored image, or null on failure.
*/
async function downloadAndStoreImage(
supabase: Awaited<ReturnType<typeof createClient>>,
imageUrl: string,
wpSlug: string
): Promise<string | null> {
try {
// Fetch the image
const response = await fetch(imageUrl, {
headers: { 'User-Agent': 'BroadcastBeat-Importer/1.0' },
signal: AbortSignal.timeout(15000),
});
if (!response.ok) return null;
const contentType = response.headers.get('content-type') || 'image/jpeg';
const buffer = await response.arrayBuffer();
if (buffer.byteLength === 0) return null;
// Derive file extension from content-type or original URL
let ext = 'jpg';
if (contentType.includes('png')) ext = 'png';
else if (contentType.includes('gif')) ext = 'gif';
else if (contentType.includes('webp')) ext = 'webp';
else if (contentType.includes('svg')) ext = 'svg';
else {
// Try to get extension from URL
const urlPath = new URL(imageUrl).pathname;
const urlExt = urlPath.split('.').pop()?.toLowerCase();
if (urlExt && ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(urlExt)) {
ext = urlExt === 'jpeg' ? 'jpg' : urlExt;
}
}
const storagePath = `featured/${wpSlug}.${ext}`;
// Upload to Supabase Storage (upsert to overwrite on re-import)
const { error: uploadError } = await supabase.storage
.from(IMAGE_BUCKET)
.upload(storagePath, buffer, {
contentType,
upsert: true,
});
if (uploadError) return null;
// Get the public URL
const { data: publicUrlData } = supabase.storage
.from(IMAGE_BUCKET)
.getPublicUrl(storagePath);
return publicUrlData?.publicUrl || null;
} catch {
return null;
}
}
export async function POST(request: NextRequest) {
try {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json().catch(() => ({}));
const maxPages = body?.maxPages || 5;
// Create import log
const { data: logData, error: logError } = await supabase
.from('wp_import_logs')
.insert({ status: 'running', started_at: new Date().toISOString() })
.select()
.single();
if (logError) {
return NextResponse.json({ error: 'Failed to create import log' }, { status: 500 });
}
const logId = logData.id;
let totalFetched = 0;
let totalImported = 0;
let totalSkipped = 0;
let totalErrors = 0;
try {
// Fetch first page to get total pages
const { posts: firstPagePosts, totalPages } = await fetchWPPosts(1);
const pagesToFetch = Math.min(totalPages, maxPages);
const allPosts: WPPost[] = [...firstPagePosts];
totalFetched += firstPagePosts.length;
// Fetch remaining pages
for (let page = 2; page <= pagesToFetch; page++) {
const { posts } = await fetchWPPosts(page);
allPosts.push(...posts);
totalFetched += posts.length;
}
// Process and upsert posts
for (const post of allPosts) {
try {
const author = post._embedded?.author?.[0]?.name || 'Unknown';
const featuredMedia = post._embedded?.['wp:featuredmedia']?.[0];
const terms = post._embedded?.['wp:term'] || [];
const categories = terms.find((group) => group?.[0]?.taxonomy === 'category');
const tags = terms.find((group) => group?.[0]?.taxonomy === 'post_tag');
// Download and store the featured image in Supabase Storage
let storedImageUrl: string | null = null;
const originalImageUrl = featuredMedia?.source_url || null;
if (originalImageUrl) {
storedImageUrl = await downloadAndStoreImage(supabase, originalImageUrl, post.slug);
}
const postData = {
wp_id: post.id,
wp_slug: post.slug,
title: stripHtml(post.title?.rendered || ''),
excerpt: stripHtml(post.excerpt?.rendered || ''),
content: post.content?.rendered || '',
author_name: author,
category: categories?.[0]?.name || null,
tags: tags?.map((t) => t.name) || [],
// Use stored Supabase URL if download succeeded, fall back to original
featured_image: storedImageUrl || originalImageUrl,
featured_image_alt: featuredMedia?.alt_text || stripHtml(post.title?.rendered || ''),
status: post.status || 'published',
wp_published_at: post.date ? new Date(post.date).toISOString() : null,
};
const { error: upsertError } = await supabase
.from('wp_imported_posts')
.upsert(postData, { onConflict: 'wp_id' });
if (upsertError) {
totalErrors++;
} else {
totalImported++;
}
} catch {
totalErrors++;
}
}
// Update log as completed
await supabase
.from('wp_import_logs')
.update({
completed_at: new Date().toISOString(),
total_fetched: totalFetched,
total_imported: totalImported,
total_skipped: totalSkipped,
total_errors: totalErrors,
status: 'completed',
})
.eq('id', logId);
return NextResponse.json({
success: true,
logId,
totalFetched,
totalImported,
totalSkipped,
totalErrors,
pagesProcessed: Math.min(totalFetched > 0 ? Math.ceil(totalFetched / PER_PAGE) : 0, maxPages),
});
} catch (fetchError: any) {
await supabase
.from('wp_import_logs')
.update({
completed_at: new Date().toISOString(),
status: 'failed',
error_message: fetchError?.message || 'Unknown error',
})
.eq('id', logId);
return NextResponse.json({ error: fetchError?.message || 'Import failed' }, { status: 500 });
}
} catch (error: any) {
return NextResponse.json({ error: error?.message || 'Server error' }, { status: 500 });
}
}
export async function GET() {
try {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const [postsResult, logsResult] = await Promise.all([
supabase
.from('wp_imported_posts')
.select('id, wp_id, wp_slug, title, author_name, category, featured_image, featured_image_alt, wp_published_at, imported_at, status')
.order('wp_published_at', { ascending: false })
.limit(50),
supabase
.from('wp_import_logs')
.select('*')
.order('started_at', { ascending: false })
.limit(10),
]);
return NextResponse.json({
posts: postsResult.data || [],
logs: logsResult.data || [],
totalPosts: postsResult.data?.length || 0,
});
} catch (error: any) {
return NextResponse.json({ error: error?.message || 'Server error' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest) {
try {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json().catch(() => ({}));
const postId = body?.id;
if (!postId) {
return NextResponse.json({ error: 'Post ID required' }, { status: 400 });
}
const { error } = await supabase
.from('wp_imported_posts')
.delete()
.eq('id', postId);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error?.message || 'Server error' }, { status: 500 });
}
}