fix: add country filters and reliable ad video streaming
This commit is contained in:
@@ -6,9 +6,12 @@ import { prisma } from "@/lib/db";
|
||||
import { planFromUser } from "@/lib/plans";
|
||||
import { checkAndConsumeQuota, refundQuota } from "@/lib/quota";
|
||||
import { hizSiniriAsimi } from "@/lib/rate-limit";
|
||||
import { AD_COUNTRY_CODES } from "@/lib/countries";
|
||||
|
||||
const schema = z.object({
|
||||
searchTerm: z.string().trim().min(2).max(100),
|
||||
country: z.string().trim().toUpperCase().refine((value) => AD_COUNTRY_CODES.has(value)),
|
||||
mediaType: z.enum(["ALL", "IMAGE", "VIDEO", "MEME"]),
|
||||
maxResults: z.coerce.number().int().refine((value) => [10, 25, 50, 100].includes(value))
|
||||
});
|
||||
|
||||
@@ -57,16 +60,16 @@ export async function POST(request: Request) {
|
||||
type: "meta-ads-library",
|
||||
status: "RUNNING",
|
||||
startedAt: new Date(),
|
||||
metadata: { userId: user.id, searchTerm: parsed.data.searchTerm, maxResults: parsed.data.maxResults }
|
||||
metadata: { userId: user.id, searchTerm: parsed.data.searchTerm, country: parsed.data.country, mediaType: parsed.data.mediaType, maxResults: parsed.data.maxResults }
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const records = await runApifyActor({
|
||||
searchTerms: [parsed.data.searchTerm],
|
||||
country: "ALL",
|
||||
country: parsed.data.country,
|
||||
adActiveStatus: "ACTIVE",
|
||||
mediaType: "ALL",
|
||||
mediaType: parsed.data.mediaType,
|
||||
maxResults: parsed.data.maxResults,
|
||||
maxCostUsd: Math.max(0.1, Math.ceil(parsed.data.maxResults * 0.004 * 10) / 10),
|
||||
scrapeAdDetails: true,
|
||||
@@ -83,7 +86,7 @@ export async function POST(request: Request) {
|
||||
finishedAt: new Date(),
|
||||
recordsImported: result.imported,
|
||||
recordsFailed: result.failed,
|
||||
metadata: { userId: user.id, searchTerm: parsed.data.searchTerm, maxResults: parsed.data.maxResults, received: records.length }
|
||||
metadata: { userId: user.id, searchTerm: parsed.data.searchTerm, country: parsed.data.country, mediaType: parsed.data.mediaType, maxResults: parsed.data.maxResults, received: records.length }
|
||||
}
|
||||
});
|
||||
return NextResponse.json({ ok: true, ...result, received: records.length });
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { fetchMedia } from "@/lib/media-proxy";
|
||||
import { hizSiniriAsimi } from "@/lib/rate-limit";
|
||||
|
||||
export async function GET(request: Request, context: { params: Promise<{ creativeId: string }> }) {
|
||||
const limited = hizSiniriAsimi(request, "medya");
|
||||
if (limited) return limited;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
|
||||
const { creativeId } = await context.params;
|
||||
const creative = await prisma.adCreative.findUnique({ where: { id: creativeId }, select: { url: true, type: true } });
|
||||
if (!creative || creative.type !== "VIDEO") return NextResponse.json({ error: "MEDIA_NOT_FOUND" }, { status: 404 });
|
||||
|
||||
try {
|
||||
const upstream = await fetchMedia(creative.url, request.headers.get("range"));
|
||||
if (!upstream.ok && upstream.status !== 206) {
|
||||
await upstream.body?.cancel();
|
||||
return NextResponse.json({ error: "MEDIA_UNAVAILABLE" }, { status: 502 });
|
||||
}
|
||||
|
||||
const headers = new Headers({
|
||||
"content-type": upstream.headers.get("content-type") || "video/mp4",
|
||||
"accept-ranges": upstream.headers.get("accept-ranges") || "bytes",
|
||||
"cache-control": "private, max-age=300",
|
||||
"content-disposition": "inline",
|
||||
"x-content-type-options": "nosniff"
|
||||
});
|
||||
for (const name of ["content-length", "content-range"]) {
|
||||
const value = upstream.headers.get(name);
|
||||
if (value) headers.set(name, value);
|
||||
}
|
||||
return new Response(upstream.body, { status: upstream.status, headers });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "MEDIA_UNAVAILABLE" }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { planFromUser } from "@/lib/plans";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { AdCreativeMedia } from "@/components/ad-creative-media";
|
||||
import { ApifyEmptySearch } from "@/components/ads/apify-empty-search";
|
||||
import { AD_COUNTRIES } from "@/lib/countries";
|
||||
|
||||
export default async function AdsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {
|
||||
const user = await requireUser();
|
||||
@@ -15,11 +16,15 @@ export default async function AdsPage({ searchParams }: { searchParams: Promise<
|
||||
const q = resolvedSearchParams.q?.trim();
|
||||
const niche = resolvedSearchParams.niche;
|
||||
const mediaType = resolvedSearchParams.mediaType;
|
||||
const country = resolvedSearchParams.country?.toUpperCase();
|
||||
const displayableCreativeWhere: Prisma.AdCreativeWhereInput = { url: { not: "" } };
|
||||
const where: Prisma.AdWhereInput = { creatives: { some: displayableCreativeWhere } };
|
||||
if (q) where.OR = [{ primaryText: { contains: q, mode: "insensitive" } }, { headline: { contains: q, mode: "insensitive" } }, { brandPage: { name: { contains: q, mode: "insensitive" } } }];
|
||||
if (niche) where.niche = { contains: niche, mode: "insensitive" };
|
||||
if (mediaType) where.mediaType = mediaType as any;
|
||||
if (country && country !== "ALL") where.countries = { has: country };
|
||||
const realAdsExist = await prisma.ad.count({ where: { externalAdId: { not: { startsWith: "demo_ad_" } } } }) > 0;
|
||||
if (realAdsExist) where.externalAdId = { not: { startsWith: "demo_ad_" } };
|
||||
const ads = await prisma.ad.findMany({
|
||||
where,
|
||||
include: {
|
||||
@@ -39,12 +44,15 @@ export default async function AdsPage({ searchParams }: { searchParams: Promise<
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black">Search Meta Adlibrary</h1>
|
||||
<p className="mt-1 text-slate-500">Kazanan Meta reklamlarını keyword, niche ve medya tipine göre keşfet.</p>
|
||||
<p className="mt-1 text-slate-500">Kazanan Meta reklamlarını anahtar kelime, ülke, niş ve medya tipine göre keşfet.</p>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-white px-4 py-3 text-sm shadow-sm">Plan: <b>{plan?.name}</b> · Free ise kartlar kilitli</div>
|
||||
</div>
|
||||
<form className="mb-5 grid gap-3 rounded-3xl bg-white p-4 shadow-soft md:grid-cols-[1fr_180px_180px_120px]">
|
||||
<form className="mb-5 grid gap-3 rounded-3xl bg-white p-4 shadow-soft md:grid-cols-2 xl:grid-cols-[1fr_190px_170px_170px_120px]">
|
||||
<input name="q" defaultValue={q} placeholder="dog collar, skincare, greens..." className="rounded-2xl border border-slate-200 px-4 py-3" />
|
||||
<select name="country" defaultValue={country || "ALL"} aria-label="Ülke" className="rounded-2xl border border-slate-200 px-4 py-3">
|
||||
{AD_COUNTRIES.map(([code, label]) => <option key={code} value={code}>{label}</option>)}
|
||||
</select>
|
||||
<select name="niche" defaultValue={niche || ""} className="rounded-2xl border border-slate-200 px-4 py-3">
|
||||
<option value="">Tüm niche</option><option>Pets</option><option>Beauty</option><option>Supplements</option><option>Household</option>
|
||||
</select>
|
||||
@@ -57,7 +65,7 @@ export default async function AdsPage({ searchParams }: { searchParams: Promise<
|
||||
{["Week's biggest winners", "US winners", "Dropship Ads", "Supplements", "Top Branded"].map((x) => <span key={x} className="rounded-full bg-violet-50 px-3 py-1 text-sm font-semibold text-violet-800">{x}</span>)}
|
||||
</div>
|
||||
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
||||
{q && masked.length === 0 && <ApifyEmptySearch query={q} planLimit={apifyPlanLimit} />}
|
||||
{q && masked.length === 0 && <ApifyEmptySearch query={q} country={country || "ALL"} mediaType={mediaType || "ALL"} planLimit={apifyPlanLimit} />}
|
||||
{masked.map((ad: any) => (
|
||||
<Card key={ad.id} className="relative overflow-hidden">
|
||||
{ad.isLocked && <div className="absolute inset-0 z-10 grid place-items-center bg-white/70 backdrop-blur-[2px]"><Link href="/pricing" className="rounded-2xl bg-violet-700 px-5 py-3 font-black text-white">Start now — Unlock winners</Link></div>}
|
||||
|
||||
Reference in New Issue
Block a user