///
///
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
import { ApifyClient } from "npm:apify-client@2.1.0";
// Define types locally since Edge Functions cannot import from the client codebase
export type TrendVerdict = "POST NOW" | "WAIT" | "SKIP";
export interface TrendAnalysisResult {
trendIdentifier: string;
freshnessScore: number;
saturationScore: number;
shelfLifeEstimate: string;
verdict: TrendVerdict;
chartData: { day: string; velocity: number }[];
topVideos: { id: number; views: string; thumbnail: string }[];
}
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
'Content-Type': 'application/json',
};
// Helper function to transform raw Apify data into the required format
function transformApifyData(rawTrendIdentifier: string, rawData: any[]): TrendAnalysisResult {
const totalVideos = rawData.length;
// Placeholder logic for scores based on video count (simulating analysis)
let freshness = 50;
let saturation = 50;
let verdict: TrendVerdict = "WAIT";
if (totalVideos > 50) {
saturation = 80;
freshness = 30;
verdict = "SKIP";
} else if (totalVideos < 10) {
saturation = 20;
freshness = 90;
verdict = "POST NOW";
}
// Mock chart data (real velocity data requires historical scraping)
const chartData = [
{ day: 'Day -7', velocity: Math.floor(Math.random() * 100) },
{ day: 'Day -6', velocity: Math.floor(Math.random() * 150) },
{ day: 'Day -5', velocity: Math.floor(Math.random() * 200) },
{ day: 'Day -4', velocity: Math.floor(Math.random() * 300) },
{ day: 'Day -3', velocity: Math.floor(Math.random() * 400) },
{ day: 'Day -2', velocity: Math.floor(Math.random() * 500) },
{ day: 'Day -1', velocity: Math.floor(Math.random() * 600) },
];
const topVideos = rawData.slice(0, 3).map((video: any, index: number) => ({
id: index + 1,
views: video?.playCount && typeof video.playCount === 'number'
? (video.playCount / 1000000).toFixed(1) + 'M'
: 'N/A',
thumbnail: video?.cover || '/placeholder.svg',
}));
return {
trendIdentifier: rawTrendIdentifier,
freshnessScore: freshness,
saturationScore: saturation,
shelfLifeEstimate: "Requires deeper analysis",
verdict: verdict,
chartData: chartData,
topVideos: topVideos,
};
}
// Helper function to clean the trend identifier for Apify
function cleanTrendIdentifier(input: string): string {
// Simple cleaning: remove leading #, trim whitespace
let cleaned = input.trim();
if (cleaned.startsWith('#')) {
cleaned = cleaned.substring(1);
}
// If it looks like a URL, we might need more complex parsing,
// but for now, we assume the user is primarily inputting hashtags or clean text.
return cleaned;
}
async function getTikTokTrendData(trendIdentifier: string): Promise {
const apifyToken = Deno.env.get('APIFY_TOKEN');
if (!apifyToken) {
throw new Error("APIFY_TOKEN environment variable is not set. Please configure it in Supabase secrets.");
}
const client = new ApifyClient({ token: apifyToken });
const cleanedIdentifier = cleanTrendIdentifier(trendIdentifier);
if (!cleanedIdentifier) {
throw new Error("Invalid trend identifier provided after cleaning.");
}
// We assume the trendIdentifier is a hashtag for this example.
const actorId = 'apify/tiktok-scraper';
const runInput = {
hashtags: [cleanedIdentifier],
resultsLimit: 10, // Fetch a small sample for quick analysis
};
console.log(`Starting Apify run for actor ${actorId} with input: ${cleanedIdentifier}`);
let run;
try {
run = await client.actor(actorId).call(runInput);
} catch (e) {
console.error("Apify client call failed:", e);
// Re-throw a clearer error if the API call itself fails
throw new Error(`Apify API call failed: ${e.message}`);
}
// Check if run is properly initialized before accessing properties
if (!run) {
throw new Error("Apify actor run failed to initialize properly.");
}
console.log(`Apify run finished. Run ID: ${run.id}, Status: ${run.status}`);
if (run.status === 'FAILED') {
// This captures the specific error you saw in the logs
throw new Error(`Apify actor run failed for identifier "${cleanedIdentifier}". Please check the Apify console for details on run ID: ${run.id}.`);
}
// Fetch items from the default dataset
const { items } = await client.dataset(run.defaultDatasetId).listItems();
if (!items || items.length === 0) {
console.log(`Apify returned no data for identifier: ${cleanedIdentifier}`);
// If Apify returns no data, we still return a result indicating low activity
return {
trendIdentifier,
freshnessScore: 10,
saturationScore: 10,
shelfLifeEstimate: "No recent activity found",
verdict: "WAIT",
chartData: [],
topVideos: [],
};
}
// Transform the raw data into the required format
return transformApifyData(trendIdentifier, items);
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
// // FIXME: Check for authentication (implement proper auth based on our needs)
// const authHeader = req.headers.get('authorization');
//
// if (!authHeader || !authHeader.startsWith('Bearer ')) {
// return new Response(
// JSON.stringify({ error: 'Unauthorized' }),
// { status: 401, headers: corsHeaders }
// );
// }
//
// // Here we validate the JWT token or API key
// // For example, with Supabase JWT validation:
//
// // const token = authHeader.substring(7);
// // const { data, error } = await supabase.auth.getUser(token);
// // if (error || !data?.user) {
// // return new Response(
// // JSON.stringify({ error: 'Invalid token' }),
// // { status: 401, headers: corsHeaders }
// // );
// // }
// END FIXME WIP
// Manual authentication handling is omitted for simplicity but recommended for production
const { trendIdentifier } = await req.json();
if (!trendIdentifier) {
return new Response(JSON.stringify({ error: 'Missing trendIdentifier' }), {
status: 400,
headers: corsHeaders,
});
}
const analysisResult = await getTikTokTrendData(trendIdentifier);
return new Response(JSON.stringify(analysisResult), {
headers: corsHeaders,
status: 200,
});
} catch (error) {
console.error("Edge Function Error:", error);
return new Response(JSON.stringify({ error: error.message }), {
headers: corsHeaders,
status: 500,
});
}
});