initial commit: rocket.new export of broadcastbeat
This commit is contained in:
294
src/app/api/wp-import/route.ts
Normal file
294
src/app/api/wp-import/route.ts
Normal 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(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/ /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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user