fix: vídeos en Social y notas Superhuman por día desde Drive
Los docs de seguimiento son Google Docs embebidos: se exportan, se parten por fecha y se acumulan. Social renderiza VIDEO/REELS y trae vídeos de página. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,6 +23,8 @@ type SocialPost = {
|
||||
platform: 'facebook' | 'instagram';
|
||||
message?: string;
|
||||
mediaUrl?: string | null;
|
||||
/** Poster / still for VIDEO / REELS when mediaUrl is a video. */
|
||||
thumbnailUrl?: string | null;
|
||||
permalink?: string | null;
|
||||
createdAt?: string | null;
|
||||
likes: number;
|
||||
@@ -33,6 +35,17 @@ type SocialPost = {
|
||||
mediaType?: string | null;
|
||||
};
|
||||
|
||||
function isVideoType(t?: string | null): boolean {
|
||||
const s = (t || '').toUpperCase();
|
||||
return (
|
||||
s === 'VIDEO' ||
|
||||
s === 'REELS' ||
|
||||
s === 'REEL' ||
|
||||
s.includes('VIDEO') ||
|
||||
s === 'ANIMATED_IMAGE_VIDEO'
|
||||
);
|
||||
}
|
||||
|
||||
function insightValue(
|
||||
data: { name?: string; values?: { value?: number }[] }[] | undefined,
|
||||
name: string
|
||||
@@ -70,30 +83,75 @@ async function fetchPageBundle(token: string, pageId: string) {
|
||||
}
|
||||
|
||||
const postsRes = await fetch(
|
||||
`${META}/${encodeURIComponent(pageId)}/posts?fields=id,message,created_time,full_picture,permalink_url,shares,likes.summary(true),comments.summary(true)&limit=12&access_token=${encodeURIComponent(token)}`,
|
||||
`${META}/${encodeURIComponent(pageId)}/posts?fields=id,message,created_time,full_picture,permalink_url,status_type,shares,likes.summary(true),comments.summary(true),attachments{media_type,type,url,media,subattachments}&limit=16&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
const posts: SocialPost[] = [];
|
||||
const seen = new Set<string>();
|
||||
if (postsRes.ok) {
|
||||
const j = await postsRes.json();
|
||||
for (const p of j.data || []) {
|
||||
const att = p.attachments?.data?.[0];
|
||||
const attType = String(att?.media_type || att?.type || '').toLowerCase();
|
||||
const videoSrc =
|
||||
att?.media?.source ||
|
||||
att?.url ||
|
||||
att?.subattachments?.data?.[0]?.media?.source ||
|
||||
null;
|
||||
const isVideo =
|
||||
attType.includes('video') ||
|
||||
String(p.status_type || '').includes('video') ||
|
||||
Boolean(videoSrc && /\.mp4|video/i.test(String(videoSrc)));
|
||||
const thumb =
|
||||
p.full_picture ||
|
||||
att?.media?.image?.src ||
|
||||
att?.subattachments?.data?.[0]?.media?.image?.src ||
|
||||
null;
|
||||
posts.push({
|
||||
id: p.id,
|
||||
platform: 'facebook',
|
||||
message: p.message || '',
|
||||
mediaUrl: p.full_picture || null,
|
||||
mediaUrl: isVideo ? videoSrc || thumb : thumb,
|
||||
thumbnailUrl: thumb,
|
||||
permalink: p.permalink_url || null,
|
||||
createdAt: p.created_time || null,
|
||||
likes: Number(p.likes?.summary?.total_count || 0),
|
||||
comments: Number(p.comments?.summary?.total_count || 0),
|
||||
shares: Number(p.shares?.count || 0),
|
||||
mediaType: p.full_picture ? 'image' : 'status',
|
||||
mediaType: isVideo ? 'VIDEO' : thumb ? 'IMAGE' : 'status',
|
||||
});
|
||||
seen.add(p.id);
|
||||
}
|
||||
} else {
|
||||
profile.postsError = (await postsRes.text()).slice(0, 160);
|
||||
}
|
||||
|
||||
// Native page videos / reels often missing from /posts
|
||||
const videosRes = await fetch(
|
||||
`${META}/${encodeURIComponent(pageId)}/videos?fields=id,description,created_time,source,permalink_url,picture,length,title&limit=10&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (videosRes.ok) {
|
||||
const j = await videosRes.json();
|
||||
for (const v of j.data || []) {
|
||||
if (!v?.id || seen.has(v.id)) continue;
|
||||
posts.push({
|
||||
id: v.id,
|
||||
platform: 'facebook',
|
||||
message: v.description || v.title || '',
|
||||
mediaUrl: v.source || v.picture || null,
|
||||
thumbnailUrl: v.picture || null,
|
||||
permalink: v.permalink_url || null,
|
||||
createdAt: v.created_time || null,
|
||||
likes: 0,
|
||||
comments: 0,
|
||||
shares: 0,
|
||||
mediaType: 'VIDEO',
|
||||
});
|
||||
seen.add(v.id);
|
||||
}
|
||||
}
|
||||
|
||||
return { ...profile, pageInsights, posts };
|
||||
}
|
||||
|
||||
@@ -137,18 +195,29 @@ async function fetchIgBundle(token: string, igId: string) {
|
||||
}
|
||||
|
||||
const mediaRes = await fetch(
|
||||
`${META}/${encodeURIComponent(igId)}/media?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count&limit=12&access_token=${encodeURIComponent(token)}`,
|
||||
`${META}/${encodeURIComponent(igId)}/media?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count&limit=16&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
const posts: SocialPost[] = [];
|
||||
if (mediaRes.ok) {
|
||||
const j = await mediaRes.json();
|
||||
for (const m of j.data || []) {
|
||||
const mediaType = m.media_type || null;
|
||||
const video = isVideoType(mediaType);
|
||||
// VIDEO/REELS: keep media_url for <video>; thumbnail for poster
|
||||
const mediaUrl = video
|
||||
? m.media_url || m.thumbnail_url || null
|
||||
: m.media_url || m.thumbnail_url || null;
|
||||
const thumbnailUrl = m.thumbnail_url || (video ? null : m.media_url) || null;
|
||||
let reach: number | null = null;
|
||||
let impressions: number | null = null;
|
||||
let shares = 0;
|
||||
try {
|
||||
const metrics = video
|
||||
? 'reach,saved,shares,plays'
|
||||
: 'reach,impressions,saved,shares';
|
||||
const mi = await fetch(
|
||||
`${META}/${encodeURIComponent(m.id)}/insights?metric=reach,impressions,saved,shares&access_token=${encodeURIComponent(token)}`,
|
||||
`${META}/${encodeURIComponent(m.id)}/insights?metric=${metrics}&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (mi.ok) {
|
||||
@@ -158,23 +227,10 @@ async function fetchIgBundle(token: string, igId: string) {
|
||||
values?: { value?: number }[];
|
||||
}[];
|
||||
reach = insightValue(data, 'reach');
|
||||
impressions = insightValue(data, 'impressions');
|
||||
const shares = insightValue(data, 'shares');
|
||||
posts.push({
|
||||
id: m.id,
|
||||
platform: 'instagram',
|
||||
message: m.caption || '',
|
||||
mediaUrl: m.media_url || m.thumbnail_url || null,
|
||||
permalink: m.permalink || null,
|
||||
createdAt: m.timestamp || null,
|
||||
likes: Number(m.like_count || 0),
|
||||
comments: Number(m.comments_count || 0),
|
||||
shares: shares ?? 0,
|
||||
reach,
|
||||
impressions,
|
||||
mediaType: m.media_type || null,
|
||||
});
|
||||
continue;
|
||||
impressions =
|
||||
insightValue(data, 'impressions') ??
|
||||
insightValue(data, 'plays');
|
||||
shares = insightValue(data, 'shares') ?? 0;
|
||||
}
|
||||
} catch {
|
||||
/* fall through without per-media insights */
|
||||
@@ -183,13 +239,16 @@ async function fetchIgBundle(token: string, igId: string) {
|
||||
id: m.id,
|
||||
platform: 'instagram',
|
||||
message: m.caption || '',
|
||||
mediaUrl: m.media_url || m.thumbnail_url || null,
|
||||
mediaUrl,
|
||||
thumbnailUrl,
|
||||
permalink: m.permalink || null,
|
||||
createdAt: m.timestamp || null,
|
||||
likes: Number(m.like_count || 0),
|
||||
comments: Number(m.comments_count || 0),
|
||||
shares: 0,
|
||||
mediaType: m.media_type || null,
|
||||
shares,
|
||||
reach,
|
||||
impressions,
|
||||
mediaType,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user