Skip to content

Commit 29ba928

Browse files
committed
Beginning router implementation
1 parent aedd211 commit 29ba928

14 files changed

+460
-31
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.DS_Store
22
node_modules/
3-
bun.lockb
3+
bun.lockb
4+
.env.json

README.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# MyTube
2+
3+
This is a proof of concept to a larger generalization. The concept is that every website should provide a client api meant to be used in the console. You can just deliver the api as a namespaced word such as *MyTube*.
4+
5+
We are creating a youtube clone that pulls down videos from youtube and allows you to conrol it easily via commands. When creating new api commands, it's important to think about the intent rather than the actions someone would do normally.
6+
7+
For instance, if I wanted to read through the comments on a regular youtube page, you would scroll through the page and click on the comments to interact. Following this pattern won't work well for the command line. So what is the intent? To read through each comment and possibly react to it.
8+
9+
So one possible journey would be.
10+
11+
```javascript
12+
// For convenience
13+
const mt = MyTube;
14+
15+
const comments = mt.getComments();
16+
17+
// returns the current comment
18+
comments.comment();
19+
```
20+
Returns
21+
>According to GitRoll I'm a "Senior-level AI/ML Developer". I've never touched AI/ML but I expect a massive pay rise any second now
22+
23+
Then you could do
24+
```javascript
25+
comments.nextComment();
26+
```
27+
> This just proves that it's not AI that's the problem, it's people.
28+
29+
Say I want to reply
30+
```javascript
31+
comments.commment().reply("That's one way of looking at it.");
32+
```
33+
34+
Or thumbs up
35+
```javascript
36+
comments.comment().thumbsUp();
37+
```
38+
39+
40+
## Other API Ideas (This can change)
41+
* Video
42+
* Play, Pause, Stop
43+
* RelatedVideos
44+
* related, nextRelated
45+
* Select
46+
* Search
47+
* Sign In
48+
* Update Profile
49+
* Notification
50+
* notification, nextNotification
51+
* Mark Read
52+
53+
54+
Something to keep in mind. YouTube is one example where you couldn't replicate all the features in the terminal only. You need to be able to watch videos. And ideally you would see the video thumbnails before selecting a video. It would be interesting to see how much can be done via the console without having to leave the current video screen.

constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ export enum CompressionTypes {
2222

2323
export enum ContentTypes {
2424
Html = "text/html",
25-
Json = "text/json",
25+
Json = "application/json",
2626
}

head.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export default function Head({ pageTitle, styles }: { pageTitle?: string, styles?: string }): string {
2+
return `
3+
<meta charset="UTF-8">
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
5+
<title>${ pageTitle || "MyTube App"}</title>
6+
<style>
7+
#dropzone {
8+
width: 75%;
9+
height: 10px;
10+
transition: height 100ms ease-out;
11+
}
12+
#dropzone.active {
13+
height: 25px;
14+
border: 4px dashed red;
15+
}
16+
body {
17+
background: black;
18+
color: white;
19+
}
20+
${styles}
21+
</style>
22+
<script src="https://unpkg.com/[email protected]"></script>
23+
<script type="module" src="./static/MyTube.js"></script>
24+
`;
25+
}

html.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default function Html({ head, body }): string {
2+
return `
3+
<!DOCTYPE html>
4+
<html lang="en">
5+
<head>
6+
${head}
7+
</head>
8+
<body>
9+
${body}
10+
</body>
11+
</html>
12+
`;
13+
}

index.ts

+56-29
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import { Headers, CompressionTypes, RequestMethod, ContentTypes } from "./constants.ts";
22
import { TodoListForm } from "./todo-list/todoListForm.ts";
33
import { TodoList, Todo } from "./todo-list/todoList.ts";
4+
import { YT_API_KEY } from "./.env.json";
5+
import Player from "./player/player.ts";
6+
import Search from "./search.ts";
7+
import Head from "./head.ts";
8+
import Html from "./html.ts";
9+
10+
enum SearchType {
11+
Channel = 'channel',
12+
Video = 'video',
13+
Playlist = 'playlist',
14+
}
415

516
class ID {
617
public id: number;
@@ -16,6 +27,7 @@ class ID {
1627

1728
const DOMAIN = 'localhost'
1829
const PORT = 8000;
30+
1931
// MAX_ORDER is the default sort order for items that haven't been sorted yet
2032
const MAX_ORDER = 999999;
2133
const id = new ID();
@@ -26,14 +38,57 @@ const server = Bun.serve({
2638
async fetch(request: Request) {
2739

2840
const path = new URL(request.url)?.pathname;
41+
const search = new URL(request.url)?.search;
42+
const params = new URLSearchParams(search);
2943

3044
if (path.lastIndexOf("static/") !== -1) {
3145
const file = Bun.file("." + path);
3246
return new Response(file);
3347
}
3448

3549
if (request.method === RequestMethod.GET) {
36-
const res: Response = compressResponse(html({ todos }), request, {
50+
const searchParams = params.has("s") && params.get("s");
51+
const typeParams = params.has("type") && params.get("type");
52+
const isValidType = typeParams && Object.values<string>(SearchType).includes(typeParams);
53+
54+
// search
55+
if (searchParams) {
56+
let url = `https://www.googleapis.com/youtube/v3/search?key=${YT_API_KEY}&part=snippet&q=${searchParams}`;
57+
58+
if (isValidType) {
59+
url += "&type=" + typeParams;
60+
}
61+
try {
62+
const ytResponse = await fetch(url);
63+
64+
if (!ytResponse.ok) throw new Error("Response status: " + ytResponse.status);
65+
66+
const json = await ytResponse.json();
67+
const html = Html({
68+
head: Head({
69+
pageTitle: "Search Results for " + searchParams,
70+
}),
71+
body: Search(json),
72+
});
73+
74+
const res: Response = compressResponse(html, request, {
75+
headers: { [Headers.ContentType]: ContentTypes.Html }
76+
});
77+
return res;
78+
79+
80+
} catch (error) {
81+
console.log("Error ocurred: ", error);
82+
}
83+
}
84+
85+
const html = Html({
86+
head: Head({
87+
pageTitle: "MyTube Home Page",
88+
}),
89+
body: Player(),
90+
})
91+
const res: Response = compressResponse(html, request, {
3792
headers: { [Headers.ContentType]: ContentTypes.Html }
3893
});
3994
return res;
@@ -90,34 +145,6 @@ const server = Bun.serve({
90145

91146
console.log(`Listening on http://${DOMAIN}:${server.port}`);
92147

93-
const html = ({ todos = [] }: { todos: Todo[] }) => `
94-
<!DOCTYPE html>
95-
<html lang="en">
96-
<head>
97-
<meta charset="UTF-8">
98-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
99-
<title>TODO APP</title>
100-
<style>
101-
#dropzone {
102-
width: 75%;
103-
height: 10px;
104-
transition: height 100ms ease-out;
105-
}
106-
#dropzone.active {
107-
height: 25px;
108-
border: 4px dashed red;
109-
}
110-
</style>
111-
<script src="https://unpkg.com/[email protected]"></script>
112-
<script src="./todo-list/static/scripts.js"></script>
113-
</head>
114-
<body>
115-
<h1>Todo List</h1>
116-
${TodoListForm(todos)}
117-
</body>
118-
</html>
119-
`;
120-
121148
function compressResponse(body: string, request: Request, options: ResponseInit): Response {
122149
const clientAcceptsGzip: boolean = request.headers.get(Headers.AcceptEncoding)?.indexOf(CompressionTypes.Gzip) !== -1;
123150

player/player.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function Player() {
2+
return (`
3+
<div id="player"></div>
4+
<script type="module" src="./player/static/scripts.js"></script>
5+
`)
6+
}

player/static/scripts.js

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
let player;
2+
const urlParams = new URLSearchParams(window.location.search);
3+
const videoId = urlParams ? urlParams.get("v") : "M7lc1UVf-VE";
4+
5+
const tag = document.createElement('script');
6+
7+
tag.src = "https://www.youtube.com/iframe_api";
8+
const firstScriptTag = document.getElementsByTagName('script')[0];
9+
firstScriptTag?.parentNode?.insertBefore(tag, firstScriptTag);
10+
11+
12+
window.onYouTubeIframeAPIReady = function() {
13+
player = new YT.Player('player', {
14+
height: '390',
15+
width: '640',
16+
videoId,
17+
playerVars: {
18+
'playsinline': 1
19+
},
20+
events: {
21+
'onReady': onPlayerReady,
22+
'onStateChange': onPlayerStateChange
23+
}
24+
});
25+
}
26+
27+
function onPlayerReady(event) {
28+
event.target.playVideo();
29+
}
30+
31+
let done = false;
32+
function onPlayerStateChange(event) {
33+
if (event.data == YT.PlayerState.PLAYING && !done) {
34+
setTimeout(player.stopVideo, 6000);
35+
done = true;
36+
}
37+
}
38+
39+
function stop() {
40+
player.stopVideo();
41+
}
42+
43+
function play() {
44+
player.playVideo();
45+
}
46+
47+
function pause() {
48+
player.pauseVideo();
49+
}
50+
51+
function next() {
52+
player.nextVideo();
53+
}
54+
55+
function prev() {
56+
player.previousVideo();
57+
}
58+
59+
function mute() {
60+
if (player.isMuted()) {
61+
player.unMute();
62+
} else {
63+
player.mute();
64+
}
65+
}
66+
67+
export default {
68+
onPlayerReady,
69+
onPlayerStateChange,
70+
stop,
71+
play,
72+
pause,
73+
next,
74+
prev,
75+
mute,
76+
}

router.test.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { expect, test } from "bun:test";
2+
import Router from "./router.ts";
3+
4+
const router = new Router();
5+
6+
test("Register multiple static get routes", () => {
7+
router.Get("/users", handler);
8+
router.Get("/users/profile", handler);
9+
router.Get("/users/profile/update", handler);
10+
router.Get("/users/profile/settings", handler);
11+
console.log('router', router);
12+
// TODO: expect(router.match("users/profile/update").toExecute(handler))
13+
});
14+
15+
test("Register multiple static get routes with new intermediate paths", () => {
16+
router.Get("/users", handler);
17+
router.Get("/users/profile/update", handler);
18+
});
19+
20+
21+
function handler() {
22+
console.log('handler', router);
23+
}

0 commit comments

Comments
 (0)