Skip to content

Commit f07db5f

Browse files
authored
Merge pull request #18072 from ethereum/fix/video-thumbnails-github-fetch
fix(videos): fetch frontmatter via GitHub API
2 parents 86b9b06 + c2a7fce commit f07db5f

1 file changed

Lines changed: 88 additions & 12 deletions

File tree

src/data-layer/fetchers/fetchVideoThumbnails.ts

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1-
import { readdir, readFile } from "fs/promises"
2-
import { join } from "path"
3-
41
import matter from "gray-matter"
52

63
import type { VideoFrontmatter } from "@/lib/interfaces"
74

8-
import { CONTENT_DIR } from "@/lib/constants"
9-
105
import { uploadToS3 } from "../s3"
116

7+
import { fetchRetry } from "./fetchRetry"
8+
9+
const GITHUB_API_BASE =
10+
"https://api.github.com/repos/ethereum/ethereum-org-website"
11+
const RAW_BASE =
12+
"https://raw.githubusercontent.com/ethereum/ethereum-org-website/master"
13+
const VIDEOS_PATH_PREFIX = "public/content/videos/"
14+
const VIDEO_INDEX_SUFFIX = "/index.md"
15+
1216
const THUMBNAIL_PREFIX = "videos/thumbnails"
1317

18+
interface GitTreeItem {
19+
path: string
20+
type: "blob" | "tree"
21+
}
22+
1423
/**
1524
* Derive an S3 key extension from a URL path. Falls back to "jpg".
1625
*/
@@ -29,10 +38,75 @@ function youtubeThumbnailUrl(youtubeId: string, quality: "sd" | "hq"): string {
2938
return `https://img.youtube.com/vi/${youtubeId}/${quality}default.jpg`
3039
}
3140

41+
/**
42+
* Fetch the repo tree and return the list of video slugs (top-level entries
43+
* under public/content/videos/ that have an index.md).
44+
*/
45+
async function discoverVideoSlugs(token: string): Promise<string[]> {
46+
const url = `${GITHUB_API_BASE}/git/trees/master?recursive=1`
47+
const response = await fetchRetry(url, {
48+
headers: {
49+
Authorization: `Bearer ${token}`,
50+
Accept: "application/vnd.github.v3+json",
51+
},
52+
})
53+
54+
if (!response.ok) {
55+
throw new Error(`Failed to fetch repo tree: ${response.status}`)
56+
}
57+
58+
const data = await response.json()
59+
const tree: GitTreeItem[] = data.tree
60+
61+
const slugs: string[] = []
62+
for (const item of tree) {
63+
if (item.type !== "blob") continue
64+
if (!item.path.startsWith(VIDEOS_PATH_PREFIX)) continue
65+
if (!item.path.endsWith(VIDEO_INDEX_SUFFIX)) continue
66+
67+
const inner = item.path.slice(
68+
VIDEOS_PATH_PREFIX.length,
69+
-VIDEO_INDEX_SUFFIX.length
70+
)
71+
// Skip nested paths (none expected, kept defensive)
72+
if (!inner.includes("/")) slugs.push(inner)
73+
}
74+
return slugs
75+
}
76+
77+
/**
78+
* Fetch a single video's frontmatter from raw GitHub content.
79+
* Returns null on fetch failure so the caller can skip and continue.
80+
*/
81+
async function fetchFrontmatter(
82+
slug: string,
83+
token: string
84+
): Promise<VideoFrontmatter | null> {
85+
const url = `${RAW_BASE}/${VIDEOS_PATH_PREFIX}${slug}${VIDEO_INDEX_SUFFIX}`
86+
const response = await fetchRetry(url, {
87+
headers: { Authorization: `Bearer ${token}` },
88+
})
89+
90+
if (!response.ok) {
91+
console.warn(
92+
`[VideoThumbnails] Failed to fetch markdown for ${slug}: ${response.status}`
93+
)
94+
return null
95+
}
96+
97+
const text = await response.text()
98+
const { data } = matter(text)
99+
return data as VideoFrontmatter
100+
}
101+
32102
/**
33103
* Fetch video thumbnails from YouTube (or custom URLs) and upload to S3.
34104
* Keyed by video slug so each video maps to exactly one S3 object.
35105
*
106+
* Reads video frontmatter (youtubeId, customThumbnailUrl) from GitHub via
107+
* the API rather than the local filesystem, so the task runs cleanly on
108+
* Trigger.dev where the app's content tree isn't bundled.
109+
*
36110
* For each video:
37111
* 1. If customThumbnailUrl exists, upload that
38112
* 2. Otherwise try sddefault.jpg first (best quality)
@@ -41,19 +115,21 @@ function youtubeThumbnailUrl(youtubeId: string, quality: "sd" | "hq"): string {
41115
* Returns a map of video slug -> S3 thumbnail URL.
42116
*/
43117
export async function fetchVideoThumbnails(): Promise<Record<string, string>> {
118+
const token = process.env.GITHUB_TOKEN_READ_ONLY
119+
if (!token) {
120+
throw new Error("GitHub token not set (GITHUB_TOKEN_READ_ONLY)")
121+
}
122+
44123
console.log("Starting video thumbnail sync to S3")
45124

46-
const videosDir = join(process.cwd(), CONTENT_DIR, "videos")
47-
const entries = await readdir(videosDir, { withFileTypes: true })
48-
const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name)
125+
const slugs = await discoverVideoSlugs(token)
126+
console.log(`Found ${slugs.length} videos in repo tree`)
49127

50128
const results = await Promise.all(
51129
slugs.map(async (slug) => {
52130
try {
53-
const mdPath = join(videosDir, slug, "index.md")
54-
const raw = await readFile(mdPath, "utf-8")
55-
const { data } = matter(raw)
56-
const fm = data as VideoFrontmatter
131+
const fm = await fetchFrontmatter(slug, token)
132+
if (!fm) return null
57133

58134
// YouTube thumbs are always jpg; custom URLs derive ext from the path.
59135
const ext = fm.customThumbnailUrl

0 commit comments

Comments
 (0)