mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-31 22:29:48 +08:00
feat(baoyu-fetch): add URL reader CLI with Chrome CDP and site adapters
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
buildYouTubeThumbnailCandidates,
|
||||
formatTimestampRange,
|
||||
parseYouTubeDescriptionChapters,
|
||||
parseYouTubeVideoId,
|
||||
renderYouTubeTranscriptMarkdown,
|
||||
} from "../adapters/youtube/utils";
|
||||
|
||||
describe("parseYouTubeVideoId", () => {
|
||||
test("parses watch URLs", () => {
|
||||
expect(parseYouTubeVideoId(new URL("https://www.youtube.com/watch?v=abc123"))).toBe("abc123");
|
||||
});
|
||||
|
||||
test("parses youtu.be URLs", () => {
|
||||
expect(parseYouTubeVideoId(new URL("https://youtu.be/abc123"))).toBe("abc123");
|
||||
});
|
||||
|
||||
test("parses shorts URLs", () => {
|
||||
expect(parseYouTubeVideoId(new URL("https://www.youtube.com/shorts/abc123"))).toBe("abc123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseYouTubeDescriptionChapters", () => {
|
||||
test("extracts chapter timestamps from description lines", () => {
|
||||
expect(
|
||||
parseYouTubeDescriptionChapters(`0:00 Intro
|
||||
2:15 What is a product engineer?
|
||||
10:05 Career paths`),
|
||||
).toEqual([
|
||||
{ title: "Intro", time: 0 },
|
||||
{ title: "What is a product engineer?", time: 135 },
|
||||
{ title: "Career paths", time: 605 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("ignores isolated timestamps that do not look like chapters", () => {
|
||||
expect(parseYouTubeDescriptionChapters("Published on 2026-03-26\nSee you at 1:23")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderYouTubeTranscriptMarkdown", () => {
|
||||
test("renders description before chapters and keeps every segment on its own line", () => {
|
||||
const markdown = renderYouTubeTranscriptMarkdown({
|
||||
description: "Line one\nLine two",
|
||||
chapters: [
|
||||
{ title: "Intro", time: 0 },
|
||||
{ title: "Deep Dive", time: 4 },
|
||||
],
|
||||
segments: [
|
||||
{ start: 0, end: 2, text: "Hello everyone." },
|
||||
{ start: 2, end: 4, text: "Welcome back." },
|
||||
{ start: 4, end: 7, text: "Now the details." },
|
||||
],
|
||||
});
|
||||
|
||||
expect(markdown).toContain("## Description");
|
||||
expect(markdown).toContain("Line one \nLine two");
|
||||
expect(markdown).toContain("## Chapters");
|
||||
expect(markdown).toContain("### Intro [0:00 -> 0:04]");
|
||||
expect(markdown).toContain("[0:00 -> 0:02] Hello everyone.");
|
||||
expect(markdown).toContain("[0:02 -> 0:04] Welcome back.");
|
||||
expect(markdown).toContain("### Deep Dive [0:04 -> 0:07]");
|
||||
expect(markdown).toContain("[0:04 -> 0:07] Now the details.");
|
||||
});
|
||||
|
||||
test("falls back to a transcript section when chapters are unavailable", () => {
|
||||
const markdown = renderYouTubeTranscriptMarkdown({
|
||||
segments: [{ start: 65, end: 70, text: "Single line." }],
|
||||
chapters: [],
|
||||
});
|
||||
|
||||
expect(markdown).toContain("## Transcript");
|
||||
expect(markdown).toContain("[1:05 -> 1:10] Single line.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("thumbnail helpers", () => {
|
||||
test("prefers max resolution thumbnail candidates before listed fallbacks", () => {
|
||||
expect(
|
||||
buildYouTubeThumbnailCandidates("abc123", [
|
||||
"https://i.ytimg.com/vi/abc123/hqdefault.jpg",
|
||||
"https://i.ytimg.com/vi/abc123/mqdefault.jpg?foo=bar",
|
||||
]),
|
||||
).toEqual([
|
||||
"https://i.ytimg.com/vi/abc123/maxresdefault.jpg",
|
||||
"https://i.ytimg.com/vi/abc123/sddefault.jpg",
|
||||
"https://i.ytimg.com/vi/abc123/hqdefault.jpg",
|
||||
"https://i.ytimg.com/vi/abc123/mqdefault.jpg",
|
||||
"https://i.ytimg.com/vi/abc123/default.jpg",
|
||||
]);
|
||||
});
|
||||
|
||||
test("renders timestamp ranges with start and end values", () => {
|
||||
expect(formatTimestampRange(3661, 3675)).toBe("[1:01:01 -> 1:01:15]");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user