Back to APIs list

YouTube API examples & templates

Use these vals as a playground to view and fork YouTube API examples and templates on Val Town. Run any example below or find templates that can be used as a pre-built solution.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/** @jsxImportSource https://esm.sh/react */
import { renderToString } from "npm:react-dom/server";
export default async function(req: Request) {
return new Response(
renderToString(
<html>
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Joe Schmo</title>
</head>
<body
style={{ padding: "30px", width: "300px", margin: "0 auto", fontFamily: "sans-serif", textAlign: "center" }}
>
<h1>Joe Schmo</h1>
<p>Just an average Joe</p>
<div style={{ display: "flex", flexDirection: "column", gap: "15px" }}>
<a href="https://www.instagram.com/joeschmo" style={itemStyle}>Instagram</a>
<a href="https://github.com/joeschmo" style={itemStyle}>Github</a>
<a href="https://www.linkedin.com/in/joeschmo" style={itemStyle}>LinkedIn</a>
<a href="https://twitter.com/joeschmo" style={itemStyle}>Twitter</a>
<a href="https://www.youtube.com/joeschmo" style={itemStyle}>YouTube</a>
</div>
</body>
</html>,
),
{
headers: {
"Content-Type": "text/html",
},
},
);
}
const itemStyle = {
padding: "10px",
color: "white",
backgroundColor: "#ff748d",
borderRadius: "20px",
textDecoration: "none",
boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
};

markdown.download

Handy microservice/library to convert various data sources into markdown. Intended to make it easier to consume the web in ereaders

Introductory blog post: https://taras.glek.net/post/markdown.download/

Package: https://jsr.io/@tarasglek/markdown-download

Features

  • Apply readability
  • Further convert article into markdown to simplify it
  • Allow webpages to be viewable as markdown via curl
  • Serve markdown converted to html to browsers
  • Extract youtube subtitles

Source

https://github.com/tarasglek/markdown-download

https://www.val.town/v/taras/markdown_download

License: MIT

Usage: https://markdown.download/ + URL

Dev: https://val.markdown.download/ + URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { isProbablyReaderable, Readability } from "npm:@mozilla/readability@^0.5.0";
import { DOMParser } from "npm:linkedom@0.16.10";
import { marked } from "npm:marked@12.0.1";
import { getSubtitles } from "npm:youtube-captions-scraper@^2.0.1";
import { YouTube } from "npm:youtube-sr@4.3.11";
const isCloudflareWorker = typeof Request !== "undefined" && typeof Response !== "undefined";
// init async loading of modules
const AgentMarkdownImport = isCloudflareWorker ? import("npm:agentmarkdown@6.0.0") : null;
const TurndownService = isCloudflareWorker ? null : await import("npm:turndown@^7.1.3");
/**
* converts HTML to markdown
* @returns markdown in string
*/
export async function html2markdown(html: string): Promise<string> {
if (AgentMarkdownImport) {
// TurndownService doesn't work on cf
// Dynamically import AgentMarkdown when running in Cloudflare Worker
const { AgentMarkdown } = await AgentMarkdownImport;
return await AgentMarkdown.produce(html);
} else {
// Dynamically import TurndownService otherwise
return new (await TurndownService)().turndown(html);
}
}
/**
* extracts article from html
* then converts it to md
* @returns markdown in string
*/
export async function readability2markdown(html: string): Promise<{ title: string; markdown: string }> {
const doc = await (new DOMParser().parseFromString(html, "text/html"));
const reader = new Readability(doc);
const article = reader.parse();
const markdown = await html2markdown(article?.content || "");
return { title: doc.title.textContent, markdown };
}
function getYoutubeVideoID(url: URL): string | null {
const regExp = /(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i;
const match = url.href.match(regExp);
return match ? match[1] : null;
}
function response(message: string, contentType = "text/markdown"): Response {
const headers = new Headers();
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
headers.set("Access-Control-Max-Age", "86400");
headers.set("Content-Type", contentType);
return new Response(message, {
status: 200,
headers: headers,
});
}
function err(msg: string): Response {
const errorMessage = JSON.stringify({
error: {
message: msg,
code: 400,
},
});
return response(errorMessage, "application/json");
}
function fudgeURL(url: string) {
try {
return new URL(url);
} catch (e) {
// console.log("Url parsing failed", e.stack);
return new URL("https://" + url);
}
}
function processInput(req: Request) {
let ret = {
url: undefined as undefined | URL,
response: undefined as undefined | Response,
};
const myurl = new URL(req.url);
let pathname = myurl.pathname.substring(1) + myurl.search;
if (!pathname.startsWith("http")) {
const urlAsFormParam = myurl.searchParams.get("url");
if (urlAsFormParam) {
pathname = urlAsFormParam;
} else if (pathname.length < 2) {
ret.response = response(
generate_ui(
"URL to convert to markdown:",
"https://www.val.town/v/taras/markdown_download",
"markdown.download",
),
"text/html",

stevekrouse.com - my personal website

This val hosts my personal website. The view data is stored in Val Town SQLite - @std/sqlite.

It used to live on Github Pages, which is why I proxy over requests to certain blog posts over to the Github Pages site still.

Todos

  • Speed up page load by loading sqlite data later like in @healeycodes/steve_web
  • Store more (legally storable) analytics data, and maybe make a sparkline!
  • Add some sort of way to contact me
  • Move over all my blog posts from Github Pages (maybe into @std/blob as a CMS?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { email } from "https://esm.town/v/std/email?v=11";
import { sqlite } from "https://esm.town/v/std/sqlite?v=5";
import { ReloadScriptReactElement } from "https://esm.town/v/stevekrouse/ReloadScript";
import tailwindURL from "https://esm.town/v/stevekrouse/tailwindURL";
import { renderToString } from "npm:react-dom/server";
const linkClass = "text-blue-500 hover:underline";
const Link = (
{ children, href }: {
children?: React.ReactNode;
href: string;
},
) => <a className={linkClass} href={href}>{children}</a>;
const dateClass = "text-xs text-gray-400 font-mono mr-1 hidden sm:inline-block";
async function getHits() {
const [, , { rows: [[allHits]] }, { rows: [[todayHits]] }] = await sqlite.batch([
"CREATE TABLE IF NOT EXISTS stevekrouse_com_hits (timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)",
"INSERT INTO stevekrouse_com_hits DEFAULT VALUES",
"SELECT COUNT(*) FROM stevekrouse_com_hits where timestamp > datetime('now', '-28 day')",
"SELECT COUNT(*) from stevekrouse_com_hits where timestamp > datetime('now', '-1 day')",
]);
if (allHits % 100 === 0) email({ subject: `You got ${todayHits} hits today! (${allHits} total)` });
return { allHits, todayHits };
}
export default async (request: Request) => {
const url = new URL(request.url);
if (url.pathname === "/favicon.ico") return new Response(null, { status: 404 });
if (url.pathname !== "/")
return fetch(
`https://stevekrouse.github.io/${url.pathname}${url.search}`,
request as any as RequestInit,
);
const { allHits, todayHits } = await getHits();
return new Response(
renderToString(
<html>
<head>
<title>Steve Krouse</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<script src={tailwindURL} />
{url.searchParams.get("dev")
? <ReloadScriptReactElement vals={[{ valName: "dot_com", userHandle: "stevekrouse" }]} />
: null}
</head>
<body>
<div className="max-w-3xl p-10 space-y-4 mx-auto">
<h1 className="text-orange-600 text-2xl font-medium">Steve Krouse</h1>
<div>
👋 Hi, I'm Steve. I live in Prospect Heights, Brooklyn.
</div>
<div>
I build <Link href="https://val.town">Val Town</Link>, a social website to code in the cloud.
</div>
<div>
This site was{" "}
<Link href="https://www.val.town/v/stevekrouse/dot_com">built in Val Town</Link>. It was viewed{" "}
{todayHits} times today, and {allHits} times this month.
</div>
<div>
<h2 className="text-xl mb-1">Projects</h2>
<ul>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Jul -</span>
<Link href="https://val.town">Val Town</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Sep -</span>
<Link href="https://dateme.directory">Date Me Directory</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Jan -</span>
<Link href="https://twitter.com/stevekrouse/status/1520162279899078657">Zaplib</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2021 Mar -</span>
<Link href="http://updates.compose.run">Compose</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2017 Jul -</span>
<Link href="https://futureofcoding.org/">Future of Coding</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2016 May -</span>
<Link href="https://github.com/stevekrouse/woofjs">WoofJS</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2015 Sep -</span>
<Link href="http://coding.space">The Coding Space Curriculum</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2015 Jul -</span>
<Link href="http://thecodingspace.com">The Coding Space</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2014 Jan -</span>Software Engineer @{" "}
<Link href="https://looker.com/">Looker</Link>
</li>

stevekrouse.com - my personal website

This val hosts my personal website. The view data is stored in Val Town SQLite - @std/sqlite.

It used to live on Github Pages, which is why I proxy over requests to certain blog posts over to the Github Pages site still.

Todos

  • Speed up page load by loading sqlite data later like in @healeycodes/steve_web
  • Store more (legally storable) analytics data, and maybe make a sparkline!
  • Add some sort of way to contact me
  • Move over all my blog posts from Github Pages (maybe into @std/blob as a CMS?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { email } from "https://esm.town/v/std/email?v=11";
import { sqlite } from "https://esm.town/v/std/sqlite?v=5";
import { ReloadScriptReactElement } from "https://esm.town/v/stevekrouse/ReloadScript";
import tailwindURL from "https://esm.town/v/stevekrouse/tailwindURL";
import React from "npm:react";
import { renderToString } from "npm:react-dom/server";
const linkClass = "text-blue-500 hover:underline";
const Link = (
{ children, href }: {
children?: React.ReactNode;
href: string;
},
) => <a className={linkClass} href={href}>{children}</a>;
const dateClass = "text-xs text-gray-400 font-mono mr-1 hidden sm:inline-block";
async function getHits() {
const [, , { rows: [[allHits]] }, { rows: [[todayHits]] }] = await sqlite.batch([
"CREATE TABLE IF NOT EXISTS stevekrouse_com_hits (timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)",
"INSERT INTO stevekrouse_com_hits DEFAULT VALUES",
"SELECT COUNT(*) FROM stevekrouse_com_hits where timestamp > datetime('now', '-28 day')",
"SELECT COUNT(*) from stevekrouse_com_hits where timestamp > datetime('now', '-1 day')",
]);
if (allHits % 100 === 0) email({ subject: `You got ${todayHits} hits today! (${allHits} total)` });
return { allHits, todayHits };
}
export default async (request: Request) => {
const url = new URL(request.url);
console.log(url);
if (url.pathname === "/favicon.ico") return new Response(null, { status: 404 });
if (url.pathname !== "/")
return fetch(
`https://stevekrouse.github.io/${url.pathname}${url.search}`,
request as any as RequestInit,
);
const { allHits, todayHits } = await getHits();
return new Response(
renderToString(
<html>
<head>
<title>Steve Krouse</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<script src={tailwindURL} />
{url.searchParams.get("dev")
? <ReloadScriptReactElement vals={[{ valName: "dot_com", userHandle: "stevekrouse" }]} />
: null}
</head>
<body>
<div className="max-w-3xl p-10 space-y-4 mx-auto">
<h1 className="text-orange-600 text-2xl font-medium">Steve Krouse</h1>
<div>
👋 Hi, I'm Steve. I live in Prospect Heights, Brooklyn.
</div>
<div>
I build <Link href="https://val.town">Val Town</Link>, a social website to code in the cloud.
</div>
<div>
This site was{" "}
<Link href="https://www.val.town/v/stevekrouse/dot_com">built in Val Town</Link>. It was viewed{" "}
{todayHits} times today, and {allHits} times this month.
</div>
<div>
<h2 className="text-xl mb-1">Projects</h2>
<ul>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Jul -</span>
<Link href="https://val.town">Val Town</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Sep -</span>
<Link href="https://dateme.directory">Date Me Directory</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Jan -</span>
<Link href="https://twitter.com/stevekrouse/status/1520162279899078657">Zaplib</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2021 Mar -</span>
<Link href="http://updates.compose.run">Compose</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2017 Jul -</span>
<Link href="https://futureofcoding.org/">Future of Coding</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2016 May -</span>
<Link href="https://github.com/stevekrouse/woofjs">WoofJS</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2015 Sep -</span>
<Link href="http://coding.space">The Coding Space Curriculum</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2015 Jul -</span>
<Link href="http://thecodingspace.com">The Coding Space</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2014 Jan -</span>Software Engineer @{" "}

markdown.download

Handy microservice/library to convert various data sources into markdown. Intended to make it easier to consume the web in ereaders

https://jsr.io/@tarasglek/markdown-download

Features

  • Apply readability
  • Further convert article into markdown to simplify it
  • Allow webpages to be viewable as markdown via curl
  • Serve markdown converted to html to browsers
  • Extract youtube subtitles

Source

https://github.com/tarasglek/markdown-download

https://www.val.town/v/taras/markdown_download

License: MIT

Usage: https://markdown.download/ + URL

Dev: https://val.markdown.download/ + URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { isProbablyReaderable, Readability } from "npm:@mozilla/readability@^0.5.0";
import { DOMParser } from "npm:linkedom@0.16.10";
import { marked } from "npm:marked@12.0.1";
import { getSubtitles } from "npm:youtube-captions-scraper@^2.0.1";
const isCloudflareWorker = typeof Request !== "undefined" && typeof Response !== "undefined";
// init async loading of modules
const AgentMarkdownImport = isCloudflareWorker ? import("npm:agentmarkdown@6.0.0") : null;
const TurndownService = isCloudflareWorker ? null : await import("npm:turndown@^7.1.3");
async function markdown2html(html: string): Promise<string> {
if (AgentMarkdownImport) {
// TurndownService doesn't work on cf
// Dynamically import AgentMarkdown when running in Cloudflare Worker
const { AgentMarkdown } = await AgentMarkdownImport;
return await AgentMarkdown.produce(html);
} else {
// Dynamically import TurndownService otherwise
return new (await TurndownService)().turndown(html);
}
}
function getYoutubeVideoID(url: URL): string | null {
const regExp = /(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i;
const match = url.href.match(regExp);
return match ? match[1] : null;
}
function response(message: string, contentType = "text/markdown"): Response {
const headers = new Headers();
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
headers.set("Access-Control-Max-Age", "86400");
headers.set("Content-Type", contentType);
return new Response(message, {
status: 200,
headers: headers,
});
}
function err(msg: string): Response {
const errorMessage = JSON.stringify({
error: {
message: msg,
code: 400,
},
});
return response(errorMessage, "application/json");
}
function fudgeURL(url: string) {
try {
return new URL(url);
} catch (e) {
// console.log("Url parsing failed", e.stack);
return new URL("https://" + url);
}
}
function processInput(req: Request) {
let ret = {
url: undefined as undefined | URL,
response: undefined as undefined | Response,
};
const myurl = new URL(req.url);
let pathname = myurl.pathname.substring(1) + myurl.search;
if (!pathname.startsWith("http")) {
const urlAsFormParam = myurl.searchParams.get("url");
if (urlAsFormParam) {
pathname = urlAsFormParam;
} else if (pathname.length < 2) {
ret.response = response(
generate_ui(
"URL to convert to markdown:",
"https://www.val.town/v/taras/markdown_download",
"markdown.download",
),
"text/html",
);
return ret;
}
}
ret.url = fudgeURL(pathname);
return ret;
}
export default async function(req: Request): Promise<Response> {
const action = processInput(req);
const url = action.url;
if (!url) {
return action.response!;
}
const youtubeVideoID = getYoutubeVideoID(url);
if (youtubeVideoID) {
const arr = (await getSubtitles({
videoID: youtubeVideoID,
})) as { text: string }[];
const description = "## Generated Transcription\n\n"

markdown.download

Handy microservice/library to convert various data sources into markdown. Intended to make it easier to consume the web in ereaders

https://jsr.io/@tarasglek/markdown-download

Features

  • Apply readability
  • Further convert article into markdown to simplify it
  • Allow webpages to be viewable as markdown via curl
  • Serve markdown converted to html to browsers
  • Extract youtube subtitles

Source

https://github.com/tarasglek/markdown-download

https://www.val.town/v/taras/markdown_download

License: MIT

Usage: https://markdown.download/ + URL

Dev: https://val.markdown.download/ + URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { isProbablyReaderable, Readability } from "npm:@mozilla/readability@^0.5.0";
import { DOMParser } from "npm:linkedom@0.16.10";
import { marked } from "npm:marked@12.0.1";
import { getSubtitles } from "npm:youtube-captions-scraper@^2.0.1";
const isCloudflareWorker = typeof Request !== "undefined" && typeof Response !== "undefined";
// init async loading of modules
const AgentMarkdownImport = isCloudflareWorker ? import("npm:agentmarkdown@6.0.0") : null;
const TurndownService = isCloudflareWorker ? null : await import("npm:turndown@^7.1.3");
async function markdown2html(html: string): Promise<string> {
if (AgentMarkdownImport) {
// TurndownService doesn't work on cf
// Dynamically import AgentMarkdown when running in Cloudflare Worker
const { AgentMarkdown } = await AgentMarkdownImport;
return await AgentMarkdown.produce(html);
} else {
// Dynamically import TurndownService otherwise
return new (await TurndownService)().turndown(html);
}
}
function getYoutubeVideoID(url: URL): string | null {
const regExp = /(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i;
const match = url.href.match(regExp);
return match ? match[1] : null;
}
function response(message: string, contentType = "text/markdown"): Response {
const headers = new Headers();
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
headers.set("Access-Control-Max-Age", "86400");
headers.set("Content-Type", contentType);
return new Response(message, {
status: 200,
headers: headers,
});
}
function err(msg: string): Response {
const errorMessage = JSON.stringify({
error: {
message: msg,
code: 400,
},
});
return response(errorMessage, "application/json");
}
function fudgeURL(url: string) {
try {
return new URL(url);
} catch (e) {
// console.log("Url parsing failed", e.stack);
return new URL("https://" + url);
}
}
function processInput(req: Request) {
let ret = {
url: undefined as undefined | URL,
response: undefined as undefined | Response,
};
const myurl = new URL(req.url);
let pathname = myurl.pathname.substring(1) + myurl.search;
if (!pathname.startsWith("http")) {
const urlAsFormParam = myurl.searchParams.get("url");
if (urlAsFormParam) {
pathname = urlAsFormParam;
} else if (pathname.length < 2) {
ret.response = response(
generate_ui(
"URL to convert to markdown:",
"https://www.val.town/v/taras/markdown_download",
"markdown.download",
),
"text/html",
);
return ret;
}
}
ret.url = fudgeURL(pathname);
return ret;
}
export default async function(req: Request): Promise<Response> {
const action = processInput(req);
const url = action.url;
if (!url) {
return action.response!;
}
const youtubeVideoID = getYoutubeVideoID(url);
if (youtubeVideoID) {
const arr = (await getSubtitles({
videoID: youtubeVideoID,
})) as { text: string }[];
const description = "## Generated Transcription\n\n"

Update Tidbyt workouts

Updates a Tidbyt with workout information:

Tidbyt_Workouts_Backup_2.00_06_13_08.Still001.png

For more information, watch the YouTube video or reach out to me on Twitter!

Usage

  1. Fork this val.
  2. Update the byDay variable to get your workout information for each day of the current and previous week.
  3. If you want to use a rule different than "Don't skip twice", you can fork and update the weekWorkoutIcons val.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { createTidbytWorkoutsImage } from "https://esm.town/v/andreterron/createTidbytWorkoutsImage";
import { setTidbytImage } from "https://esm.town/v/andreterron/setTidbytImage";
import { weekWorkoutIcons } from "https://esm.town/v/andreterron/weekWorkoutIcons";
import { workedOutByDay } from "https://esm.town/v/andreterron/workedOutByDay";
import { blob } from "https://esm.town/v/std/blob?v=11";
export let updateTidbytWorkout = async (force: boolean = false) => {
const timezone = "America/Los_Angeles";
// byDay is a Record<"YYYY-MM-DD", truthy | falsy> that
// stores which days you worked out. Recommended to get
// the current and previous week of data.
// e.g.: {"2023-11-15": true, "2023-11-13": true}
const byDay = await workedOutByDay(timezone);
// icons is an Array with one icon type for each of the 7 days
// e.g.: ["done", "skipped", "done", "done", "today", "future", "future"]
const icons = weekWorkoutIcons(byDay, timezone);
// Don't update the image if it didn't change
let iconsCache: string[] | undefined;
try {
iconsCache = await blob.getJSON("tidbytWorkoutCache");
} catch (e) {
console.error(e);
}
await blob.setJSON("tidbytWorkoutCache", icons);
if (!force && iconsCache && iconsCache.every((v, i) => v === icons[i])) {
console.log("No changes detected, skipping updating tidbyt app");
return;
}
// img is the resulting jimp image
const img = await createTidbytWorkoutsImage(icons);
// Send the image to Tidbyt
await setTidbytImage({
image: (await img.getBufferAsync(img.getMIME())).toString("base64"),
});
};

This is a deno/valtown port in progress of https://github.com/tarasglek/scrape2md

License: MIT

Handy script to scrape various data sources into markdown. Intended to feed llms in https://chatcraft.org

Usage: https://taras-scrape2md.web.val.run/ + URL_TO_SCRAPE

Or just visit in browser and paste your url

TODO

https://chatcraft.org/api/share/tarasglek/IDYChVAilfePgVZb_T5pH POST from browser https://www.val.town/v/nbbaier/valToGH sync to github

Metadata for use with https://github.com/tarasglek/valtown2js:

{
  "typeCheck": false,
  "mappings": {
    "https://esm.sh/linkedom": {
      "name": "linkedom",
      "version": "^0.16.8"
    }
  },
  "package": {
    "name": "scrape2md",
    "version": "1.0.0",
    "devDependencies": {
      "@types/turndown": "^5.0.4"
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { isProbablyReaderable, Readability } from "npm:@mozilla/readability@^0.5.0";
import { DOMParser } from "npm:linkedom@0.16.10";
import { marked } from "npm:marked@12.0.1";
import TurndownService from "npm:turndown@^7.1.3";
import { getSubtitles } from "npm:youtube-captions-scraper@^2.0.1";
function getYoutubeVideoID(url: URL): string | null {
const regExp = /(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i;
const match = url.href.match(regExp);
return match ? match[1] : null;
}
function response(message: string, contentType = "text/markdown"): Response {
const headers = new Headers();
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
headers.set("Access-Control-Max-Age", "86400");
headers.set("Content-Type", contentType);
return new Response(message, {
status: 200,
headers: headers,
});
}
function err(msg: string): Response {
const errorMessage = JSON.stringify({
error: {
message: msg,
code: 400,
},
});
return response(errorMessage, "application/json");
}
export default async function(req: Request): Promise<Response> {
const myurl = new URL(req.url);
let pathname = myurl.pathname.substring(1) + myurl.search;
if (!pathname.startsWith("http")) {
const urlAsFormParam = myurl.searchParams.get("url");
if (urlAsFormParam) {
pathname = urlAsFormParam;
} else {
return response(html, "text/html");
}
}
const url = new URL(pathname);
const youtubeVideoID = getYoutubeVideoID(url);
if (youtubeVideoID) {
const arr = (await getSubtitles({
videoID: youtubeVideoID,
})) as { text: string }[];
const description = "## Generated Transcription\n\n"
+ arr.map(({ text }) => text).join("\n");
return response(description);
}
const dom_promise = fetch(url.toString(), {
method: req.method,
headers: new Headers({
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-User": "?1",
"Sec-Fetch-Dest": "document",
"Referer": "https://www.google.com/",
"sec-ch-ua": `"Not A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"`,
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": `"macOS"`,
"Upgrade-Insecure-Requests": "1",
// Add any other headers you need here
}),
})
.then(r => r.text())
.then(async html => new DOMParser().parseFromString(html, "text/html"));
const doc = await dom_promise;
const reader = new Readability(doc);
const article = reader.parse();
console.log("content", typeof article?.content, article?.content);
const markdown = new TurndownService().turndown(article?.content || "") + "\n\n" + url;
if (req.headers.get("Accept")?.includes("text/html")) {
return response(await marked.parse(markdown), "text/html");
} else {
return response(markdown);
}
}
const html = `
<!DOCTYPE html>
<html>
<head>
<title>scrape2md ui</title>
<!-- Tailwind CSS -->

stevekrouse.com - my personal website

This val hosts my personal website. The view data is stored in Val Town SQLite - @std/sqlite.

It used to live on Github Pages, which is why I proxy over requests to certain blog posts over to the Github Pages site still.

Todos

  • Speed up page load by loading sqlite data later like in @healeycodes/steve_web
  • Store more (legally storable) analytics data, and maybe make a sparkline!
  • Add some sort of way to contact me
  • Move over all my blog posts from Github Pages (maybe into @std/blob as a CMS?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { email } from "https://esm.town/v/std/email?v=11";
import { sqlite } from "https://esm.town/v/std/sqlite?v=5";
import { ReloadScriptReactElement } from "https://esm.town/v/stevekrouse/ReloadScript";
import tailwindURL from "https://esm.town/v/stevekrouse/tailwindURL";
import { renderToString } from "npm:react-dom/server";
const linkClass = "text-blue-500 hover:underline";
const Link = (
{ children, href }: {
children?: React.ReactNode;
href: string;
},
) => <a className={linkClass} href={href}>{children}</a>;
const dateClass = "text-xs text-gray-400 font-mono mr-1 hidden sm:inline-block";
async function getHits() {
const [, , { rows: [[allHits]] }, { rows: [[todayHits]] }] = await sqlite.batch([
"CREATE TABLE IF NOT EXISTS stevekrouse_com_hits (timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)",
"INSERT INTO stevekrouse_com_hits DEFAULT VALUES",
"SELECT COUNT(*) FROM stevekrouse_com_hits where timestamp > datetime('now', '-28 day')",
"SELECT COUNT(*) from stevekrouse_com_hits where timestamp > datetime('now', '-1 day')",
]);
if (allHits % 100 === 0) email({ subject: `You got ${todayHits} hits today! (${allHits} total)` });
return { allHits, todayHits };
}
export default async (request: Request) => {
const url = new URL(request.url);
console.log(url);
if (url.pathname === "/favicon.ico") return new Response(null, { status: 404 });
if (url.pathname !== "/")
return fetch(
`https://stevekrouse.github.io/${url.pathname}${url.search}`,
request as any as RequestInit,
);
const { allHits, todayHits } = await getHits();
return new Response(
renderToString(
<html>
<head>
<title>Steve Krouse</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<script src={tailwindURL} />
{url.searchParams.get("dev")
? <ReloadScriptReactElement vals={[{ valName: "dot_com", userHandle: "stevekrouse" }]} />
: null}
</head>
<body>
<div className="max-w-3xl p-10 space-y-4 mx-auto">
<h1 className="text-orange-600 text-2xl font-medium">Steve Krouse</h1>
<div>
👋 Hi, I'm Steve. I live in Prospect Heights, Brooklyn.
</div>
<div>
I build <Link href="https://val.town">Val Town</Link>, a social website to code in the cloud.
</div>
<div>
This site was{" "}
<Link href="https://www.val.town/v/stevekrouse/dot_com">built in Val Town</Link>. It was viewed{" "}
{todayHits} times today, and {allHits} times this month.
</div>
<div>
<h2 className="text-xl mb-1">Projects</h2>
<ul>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Jul -</span>
<Link href="https://val.town">Val Town</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Sep -</span>
<Link href="https://dateme.directory">Date Me Directory</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Jan -</span>
<Link href="https://twitter.com/stevekrouse/status/1520162279899078657">Zaplib</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2021 Mar -</span>
<Link href="http://updates.compose.run">Compose</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2017 Jul -</span>
<Link href="https://futureofcoding.org/">Future of Coding</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2016 May -</span>
<Link href="https://github.com/stevekrouse/woofjs">WoofJS</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2015 Sep -</span>
<Link href="http://coding.space">The Coding Space Curriculum</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2015 Jul -</span>
<Link href="http://thecodingspace.com">The Coding Space</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2014 Jan -</span>Software Engineer @{" "}
<Link href="https://looker.com/">Looker</Link>
Runs every 30 min
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { email } from "https://esm.town/v/std/email";
import { sqlite } from "https://esm.town/v/std/sqlite";
// await sqlite.execute(`DROP TABLE youtube_comments`);
await sqlite.execute(`
-- YouTube Comments Table
CREATE TABLE IF NOT EXISTS youtube_comments (
comment_id TEXT PRIMARY KEY,
video_id TEXT NOT NULL,
comment TEXT,
author_display_name TEXT,
author_channel_url TEXT,
published_at TEXT,
inserted_at TEXT
);
`);
const addCommentSql = `
INSERT OR IGNORE INTO youtube_comments(
comment_id,
video_id,
author_display_name,
author_channel_url,
comment,
published_at,
inserted_at
) values(
:commentId,
:videoId,
:authorDisplayName,
:authorChannelUrl,
:textDisplay,
:publishedAt,
:now
)
`;
function rowObjects(data) {
return data.rows.map(row => {
return data.columns.reduce((obj, column, index) => {
let value;
if (data.columnTypes[index] === "json") {
value = JSON.parse(row[index]);
} else {
value = row[index];
}
obj[column] = value;
return obj;
}, {});
});
}
export default async function() {
const API_KEY = Deno.env.get("YOUTUBE_API_KEY");
const CHANNEL_ID = Deno.env.get("YOUTUBE_CHANNEL_ID");
const now = (new Date()).toISOString();
const allComments = [];
async function fetchComments(pageToken: string = ""): Promise<void> {
const url =
`https://www.googleapis.com/youtube/v3/commentThreads?key=${API_KEY}&textFormat=plainText&part=snippet&allThreadsRelatedToChannelId=${CHANNEL_ID}&pageToken=${pageToken}`;
const response = await fetch(url);
const data = await response.json();
if (data.items) {
for (const item of data.items) {
allComments.push(item);
const comment = item.snippet.topLevelComment;
const args = {
commentId: comment.id,
videoId: comment.snippet.videoId,
authorDisplayName: comment.snippet.authorDisplayName,
authorChannelUrl: comment.snippet.authorChannelUrl,
textDisplay: comment.snippet.textDisplay,
publishedAt: comment.snippet.publishedAt,
now,
};
await sqlite.execute({
sql: addCommentSql,
args,
});
}
}
// If there's a next page, fetch more comments recursively
if (data.nextPageToken) {
fetchComments(data.nextPageToken);
}
}
await fetchComments();
const newComments = await sqlite.execute({
sql: `SELECT *
FROM youtube_comments
WHERE inserted_at == :now`,
args: { now },
});

Update Tidbyt workouts

Updates a Tidbyt with workout information:

Tidbyt_Workouts_Backup_2.00_06_13_08.Still001.png

For more information, watch the YouTube video or reach out to me on Twitter!

Usage

  1. Fork this val.
  2. Update the byDay variable to get your workout information for each day of the current and previous week.
  3. If you want to use a rule different than "Don't skip twice", you can fork and update the weekWorkoutIcons val.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { createTidbytWorkoutsImage } from "https://esm.town/v/andreterron/createTidbytWorkoutsImage";
import { setTidbytImage } from "https://esm.town/v/andreterron/setTidbytImage";
import { weekWorkoutIcons } from "https://esm.town/v/andreterron/weekWorkoutIcons";
import { workedOutByDay } from "https://esm.town/v/andreterron/workedOutByDay";
import { blob } from "https://esm.town/v/std/blob?v=11";
export let updateTidbytWorkout = async (force: boolean = false) => {
const timezone = "America/Los_Angeles";
// byDay is a Record<"YYYY-MM-DD", truthy | falsy> that
// stores which days you worked out. Recommended to get
// the current and previous week of data.
// e.g.: {"2023-11-15": true, "2023-11-13": true}
const byDay = await workedOutByDay(timezone);
// icons is an Array with one icon type for each of the 7 days
// e.g.: ["done", "skipped", "done", "done", "today", "future", "future"]
const icons = weekWorkoutIcons(byDay, timezone);
// Don't update the image if it didn't change
let iconsCache: string[] | undefined;
try {
iconsCache = await blob.getJSON("tidbytWorkoutCache");
} catch (e) {
console.error(e);
}
await blob.setJSON("tidbytWorkoutCache", icons);
if (!force && iconsCache && iconsCache.every((v, i) => v === icons[i])) {
console.log("No changes detected, skipping updating tidbyt app");
return;
}
// img is the resulting jimp image
const img = await createTidbytWorkoutsImage(icons);
// Send the image to Tidbyt
await setTidbytImage({
image: (await img.getBufferAsync(img.getMIME())).toString("base64"),
});
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import process from 'node:process';
import { YouTube } from 'https://deno.land/x/youtube@v0.3.0/mod.ts';
export async function fetchVideoDetails(req) {
const url = new URL(req.url);
let yturl = url.searchParams.get("yturl") || "";
let mode = url.searchParams.get("mode") || "";
let responseContent;
let publishedDate, currentDate, timeDiff, daysDiff; // Declare variables outside the switch
yturl = decodeURIComponent(yturl);
const videoIdMatch = yturl.match(/(?:https?:\/\/)?(?:www\.)?youtube\.com\/(?:watch\?v=|live\/)([a-zA-Z0-9_-]+)|youtu\.be\/([a-zA-Z0-9_-]+)/);
let videoId = videoIdMatch ? videoIdMatch[1] || videoIdMatch[2] : null;
if (videoId && videoId.includes('&')) {
videoId = videoId.split('&')[0];
}
if (!videoId) {
videoId = `0iaU9VZXKUQ`;
}
if (!mode) {
mode = `transcript`;
}
const apiKey = process.env.ytapiKey;
let apiUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&id=${videoId}&key=${apiKey}`;
try {
let response = await fetch(apiUrl);
let data = await response.json();
if (!data.items || data.items.length === 0) {
throw new Error('No video details found');
}
const item = data.items[0];
let responseContent;
switch (mode) {
case 'thumbnail':
responseContent = `![](${item.snippet.thumbnails.high.url})`;
break;
case 'channel':
const channelLink = `https://www.youtube.com/channel/${item.snippet.channelId}`;
responseContent = `[${item.snippet.channelTitle}](${channelLink})`;
break;
case 'publishDate':
responseContent = item.snippet.publishedAt;
break;
case 'viewCount':
responseContent = item.statistics.viewCount.toString();
break;
case 'viewsPerDay':
publishedDate = new Date(item.snippet.publishedAt);
currentDate = new Date();
timeDiff = currentDate.getTime() - publishedDate.getTime();
daysDiff = Math.floor(timeDiff / (1000 * 3600 * 24));
const viewsPerDay = daysDiff > 0 ? (item.statistics.viewCount / daysDiff).toFixed(2) : 'N/A';
responseContent = viewsPerDay.toString();
break;
case 'videoAge':
publishedDate = new Date(item.snippet.publishedAt);
currentDate = new Date();
timeDiff = currentDate.getTime() - publishedDate.getTime();
daysDiff = Math.floor(timeDiff / (1000 * 3600 * 24));
responseContent = daysDiff.toString();
break;
case 'videoTitle':
responseContent = item.snippet.title;
break;
case 'engagementMetrics':
const likeCount = item.statistics.likeCount || 'N/A';
const dislikeCount = item.statistics.dislikeCount || 'N/A';
const commentCount = item.statistics.commentCount || 'N/A';
responseContent = `Likes: ${likeCount}, Dislikes: ${dislikeCount}, Comments: ${commentCount}`;
break;
case 'averageViewDuration':
const duration = item.contentDetails.duration;
const durationInSeconds = parseISO8601Duration(duration);
const averageViewDuration = (durationInSeconds / item.statistics.viewCount).toFixed(2);
responseContent = `Average View Duration: ${averageViewDuration} seconds`;
break;
case 'videoCategory':
const categoryId = item.snippet.categoryId;
apiUrl = `https://www.googleapis.com/youtube/v3/videoCategories?part=snippet&id=${categoryId}&key=${apiKey}`;
response = await fetch(apiUrl);
data = await response.json();
const category = data.items && data.items.length > 0 ? data.items[0].snippet.title : 'N/A';
responseContent = `Category: ${category}`;
break;
case 'tags':
const tags = item.snippet.tags || [];
responseContent = `Tags: ${tags.join(', ')}`;
break;
case 'transcript':
async function fetchcaptions(videoId) {
const WATCH_URL = `https://www.youtube.com/watch?v=${videoId}`;
try {
const response = await fetch(WATCH_URL);
1
2
// set at Thu Nov 30 2023 14:22:53 GMT+0000 (Coordinated Universal Time)
export let topHNThreadByHour = ["Top thread on Hackernews for 3:00 is: Vespa.ai is spinning out of Yahoo as a separate company","Top thread on Hackernews for 4:00 is: President Speaking: Spoofing Alerts in 4G LTE Networks (2019) [pdf]","Top thread on Hacke
1
2
3
4
5
6
7
8
9
10
11
12
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
export const nasaImageDetails = async () => {
const nasaAPOD = await fetchJSON("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY");
let nasaImageHtml = nasaAPOD.hdurl
? `<img width="100%" src="${nasaAPOD.hdurl}"/>`
: nasaAPOD.media_type === "video"
? `<iframe width="100%" height="100%" src="${nasaAPOD.url}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>`
: `<p>Unknown Type</p><br/><code><pre>${JSON.stringify(nasaAPOD, null, 2)}</pre></code>`;
nasaImageHtml += `<p>${nasaAPOD.explanation}</p>`;
return { html: nasaImageHtml, nasaAPOD };
};
1
2
3
4
5
6
7
8
import { fetch } from "https://esm.town/v/std/fetch";
export async function getYoutubeLinkFromPage(url: string) {
const resp = await fetch(url);
const htmlText = await resp.text();
const match = htmlText.match(/https:\/\/youtube.com\/[^"]+/);
return Array.isArray(match) ? match[0] : match;
}