Skip to content

Commit 32d8fe4

Browse files
committed
update youtube views
1 parent 66881f4 commit 32d8fe4

5 files changed

Lines changed: 174 additions & 102 deletions

File tree

app/api/cron/route.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,37 @@ import type { NextRequest } from "next/server";
66
export function GET(request: NextRequest) {
77
const authHeader = request.headers.get("authorization");
88
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
9+
console.error("[CRON] Unauthorized request: invalid authorization header");
910
return new Response("Unauthorized", {
1011
status: 401,
1112
});
1213
}
13-
// Don't await just trigger
14-
console.log("youtube views triggered");
15-
fetch(publicURL() + `/api/youtube/views`, {
16-
method: "POST",
17-
headers: {
18-
authorization: `Bearer ${process.env.CRON_SECRET}`,
19-
"Cache-Control": "no-cache",
20-
},
21-
});
22-
23-
return Response.json({ success: true });
14+
try {
15+
const url = `${publicURL()}/api/youtube/views`;
16+
console.log("[CRON] Triggering YouTube views update:", url);
17+
fetch(url, {
18+
method: "POST",
19+
headers: {
20+
authorization: `Bearer ${process.env.CRON_SECRET}`,
21+
"Cache-Control": "no-cache",
22+
},
23+
})
24+
.then((res) => {
25+
if (!res.ok) {
26+
console.error("[CRON] Failed to trigger YouTube views:", res.status);
27+
} else {
28+
console.log("[CRON] Successfully triggered YouTube views update.");
29+
}
30+
})
31+
.catch((err) => {
32+
console.error("[CRON] Error triggering YouTube views:", err);
33+
});
34+
return Response.json({ success: true });
35+
} catch (err) {
36+
console.error("[CRON] Unexpected error:", err);
37+
return Response.json(
38+
{ success: false, error: String(err) },
39+
{ status: 500 },
40+
);
41+
}
2442
}

app/api/youtube/views/route.tsx

Lines changed: 69 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const fetchCache = "force-no-store";
22

3+
34
import { publicURL, youtubeParser } from "@/lib/utils";
45
import { createClient } from "next-sanity";
56
import type { NextRequest } from "next/server";
@@ -13,96 +14,91 @@ const sanityWriteClient = createClient({
1314
useCdn: false,
1415
});
1516

17+
1618
export async function POST(request: NextRequest) {
1719
const authHeader = request.headers.get("authorization");
1820
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
19-
return new Response("Unauthorized", {
20-
status: 401,
21-
});
21+
console.error("[YOUTUBE] Unauthorized request: invalid authorization header");
22+
return new Response("Unauthorized", { status: 401 });
2223
}
2324

24-
const searchParams = request.nextUrl.searchParams;
25-
const lastIdParam = searchParams.get("lastId");
26-
2725
try {
28-
// Assume if lastId is missing that the request will be the initial starting the process.
29-
const sanityRead = await sanityWriteClient.fetch(
30-
`*[youtube != null && _id > $lastId]| order(_id)[0]{
31-
_id,
32-
youtube
33-
}`,
34-
{
35-
lastId: lastIdParam || "",
36-
},
26+
// Fetch up to 10 pending/inProgress youtubeUpdateTask docs
27+
const tasks = await sanityWriteClient.fetch(
28+
`*[_type == "youtubeUpdateTask" && (status == "pending" || status == "inProgress")]| order(lastChecked asc nulls first)[0...10]{ _id, targetDoc->{_id, _type, youtube}, status }`
3729
);
3830

39-
const lastId = sanityRead?._id;
40-
41-
if (!lastId) {
42-
const message = `No doc found based on lastId ${lastId}`;
31+
if (!tasks || tasks.length === 0) {
32+
const message = `[YOUTUBE] No youtubeUpdateTask docs to process`;
4333
console.log(message);
4434
return Response.json({ success: true, message }, { status: 200 });
4535
}
4636

47-
// These should never match, if they do bail.
48-
if (lastId === lastIdParam) {
49-
console.error("lastId matches current doc, stopping calls.");
50-
return new Response("lastId matches current doc, stopping calls.", {
51-
status: 200,
52-
});
53-
}
37+
let updatedCount = 0;
38+
for (const task of tasks) {
39+
const { _id: taskId, targetDoc, status } = task;
40+
if (!targetDoc || !targetDoc.youtube) {
41+
await sanityWriteClient.patch(taskId)
42+
.set({ status: "error", errorMessage: "Missing YouTube field on targetDoc", lastChecked: new Date().toISOString() })
43+
.commit();
44+
continue;
45+
}
46+
// Mark as inProgress
47+
await sanityWriteClient.patch(taskId)
48+
.set({ status: "inProgress", lastChecked: new Date().toISOString() })
49+
.commit();
5450

55-
const id = youtubeParser(sanityRead?.youtube);
51+
const id = youtubeParser(targetDoc.youtube);
52+
if (!id) {
53+
await sanityWriteClient.patch(taskId)
54+
.set({ status: "error", errorMessage: "Invalid YouTube URL", lastChecked: new Date().toISOString() })
55+
.commit();
56+
continue;
57+
}
5658

57-
if (!id) {
58-
console.error("Missing YouTube Id");
59-
return new Response("Missing YouTube Id", { status: 404 });
60-
}
59+
try {
60+
const videoResp = await fetch(
61+
`https://www.googleapis.com/youtube/v3/videos?id=${id}&key=${process.env.YOUTUBE_API_KEY}&fields=items(id,statistics)&part=statistics`,
62+
);
63+
const json = await videoResp.json();
64+
if (videoResp.status !== 200) {
65+
await sanityWriteClient.patch(taskId)
66+
.set({ status: "error", errorMessage: JSON.stringify(json), lastChecked: new Date().toISOString() })
67+
.commit();
68+
continue;
69+
}
70+
const statistics = json?.items?.at(0)?.statistics;
71+
if (!statistics) {
72+
await sanityWriteClient.patch(taskId)
73+
.set({ status: "error", errorMessage: "No statistics found", lastChecked: new Date().toISOString() })
74+
.commit();
75+
continue;
76+
}
6177

62-
const videoResp = await fetch(
63-
`https://www.googleapis.com/youtube/v3/videos?id=${id}&key=${process.env.YOUTUBE_API_KEY}&fields=items(id,statistics)&part=statistics`,
64-
);
65-
const json = await videoResp.json();
66-
if (videoResp.status !== 200) {
67-
console.error(JSON.stringify(json));
68-
return Response.json(json, { status: videoResp.status });
69-
}
70-
console.log(JSON.stringify(json));
71-
const statistics = json?.items?.at(0)?.statistics;
78+
// Update target doc with stats
79+
await sanityWriteClient.patch(targetDoc._id)
80+
.set({
81+
"statistics.youtube.commentCount": Number.parseInt(statistics.commentCount),
82+
"statistics.youtube.favoriteCount": Number.parseInt(statistics.favoriteCount),
83+
"statistics.youtube.likeCount": Number.parseInt(statistics.likeCount),
84+
"statistics.youtube.viewCount": Number.parseInt(statistics.viewCount),
85+
})
86+
.commit();
7287

73-
if (!statistics) {
74-
const words = `No statistics found for YouTube Id ${id}`;
75-
console.error(words);
76-
return new Response(words, { status: 404 });
88+
// Mark task as completed
89+
await sanityWriteClient.patch(taskId)
90+
.set({ status: "completed", lastChecked: new Date().toISOString(), errorMessage: undefined })
91+
.commit();
92+
updatedCount++;
93+
} catch (err) {
94+
await sanityWriteClient.patch(taskId)
95+
.set({ status: "error", errorMessage: String(err), lastChecked: new Date().toISOString() })
96+
.commit();
97+
}
7798
}
78-
79-
// Update current doc with stats
80-
const sanityUpdate = await sanityWriteClient
81-
.patch(lastId)
82-
.set({
83-
"statistics.youtube.commentCount": Number.parseInt(
84-
statistics.commentCount,
85-
),
86-
"statistics.youtube.favoriteCount": Number.parseInt(
87-
statistics.favoriteCount,
88-
),
89-
"statistics.youtube.likeCount": Number.parseInt(statistics.likeCount),
90-
"statistics.youtube.viewCount": Number.parseInt(statistics.viewCount),
91-
})
92-
.commit();
93-
94-
// Trigger next call, don't wait for response
95-
fetch(publicURL() + `/api/youtube/views?lastId=${lastId}`, {
96-
method: "POST",
97-
headers: {
98-
authorization: `Bearer ${process.env.CRON_SECRET}`,
99-
"Cache-Control": "no-cache",
100-
},
101-
});
102-
103-
return Response.json(sanityUpdate);
99+
return Response.json({ success: true, updatedCount });
104100
} catch (error) {
105-
console.error(JSON.stringify(error));
106-
return Response.json({ success: false }, { status: 404 });
101+
console.error("[YOUTUBE] Unexpected error:", error);
102+
return Response.json({ success: false, error: String(error) }, { status: 500 });
107103
}
108104
}

sanity.config.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import podcastType from "@/sanity/schemas/documents/podcastType";
4141
import post from "@/sanity/schemas/documents/post";
4242
import settings from "@/sanity/schemas/singletons/settings";
4343
import sponsor from "@/sanity/schemas/documents/sponsor";
44+
import youtubeUpdateTask from "@/sanity/schemas/documents/youtubeUpdateTask";
4445
import { resolveHref } from "@/sanity/lib/utils";
4546

4647
const homeLocation = {
@@ -123,20 +124,21 @@ export default defineConfig({
123124
projectId,
124125
dataset,
125126
schema: {
126-
types: [
127-
// Singletons
128-
settings,
129-
// Documents
130-
author,
131-
course,
132-
lesson,
133-
guest,
134-
page,
135-
podcast,
136-
podcastType,
137-
post,
138-
sponsor,
139-
],
127+
types: [
128+
// Singletons
129+
settings,
130+
// Documents
131+
author,
132+
course,
133+
lesson,
134+
guest,
135+
page,
136+
podcast,
137+
podcastType,
138+
post,
139+
sponsor,
140+
youtubeUpdateTask,
141+
],
140142
},
141143
plugins: [
142144
presentationTool({
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// /sanity/schemas/documents/youtubeUpdateTask.ts
2+
import {defineField, defineType} from 'sanity'
3+
4+
export default defineType({
5+
name: 'youtubeUpdateTask',
6+
title: 'YouTube Update Task',
7+
type: 'document',
8+
fields: [
9+
defineField({
10+
name: 'targetDoc',
11+
title: 'Target Document',
12+
type: 'reference',
13+
to: [{type: 'post'}, {type: 'podcast'}],
14+
validation: Rule => Rule.required(),
15+
}),
16+
defineField({
17+
name: 'status',
18+
title: 'Status',
19+
type: 'string',
20+
options: {
21+
list: [
22+
{title: 'Pending', value: 'pending'},
23+
{title: 'In Progress', value: 'inProgress'},
24+
{title: 'Completed', value: 'completed'},
25+
{title: 'Error', value: 'error'},
26+
],
27+
layout: 'dropdown',
28+
},
29+
initialValue: 'pending',
30+
}),
31+
defineField({
32+
name: 'lastChecked',
33+
title: 'Last Checked',
34+
type: 'datetime',
35+
}),
36+
defineField({
37+
name: 'errorMessage',
38+
title: 'Error Message',
39+
type: 'text',
40+
hidden: ({parent}) => parent?.status !== 'error',
41+
}),
42+
],
43+
preview: {
44+
select: {
45+
title: 'targetDoc.title',
46+
status: 'status',
47+
lastChecked: 'lastChecked',
48+
},
49+
prepare({title, status, lastChecked}) {
50+
return {
51+
title: title ? `Update: ${title}` : 'YouTube Update Task',
52+
subtitle: `${status || 'pending'}${lastChecked ? ' | ' + new Date(lastChecked).toLocaleString() : ''}`,
53+
}
54+
},
55+
},
56+
})

vercel.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"$schema": "https://openapi.vercel.sh/vercel.json",
33
"crons": [
4-
{
5-
"path": "/api/cron",
6-
"schedule": "0 17 * * *"
7-
}
4+
{
5+
"path": "/api/cron",
6+
"schedule": "*/3 * * * *"
7+
}
88
]
99
}

0 commit comments

Comments
 (0)