Skip to content

Commit 94aeea3

Browse files
committed
feat: autocomplete for youtube (#37)
not complete yet, but wip
1 parent 7f40bd6 commit 94aeea3

File tree

10 files changed

+209
-35
lines changed

10 files changed

+209
-35
lines changed

api/src/main.rs

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
// Yes i regret using rust for this
2+
13
use actix_cors::Cors;
24
use actix_web::{App, HttpResponse, HttpServer, Responder, web};
35
use dotenvy::from_path;
4-
use sqlx::PgPool;
5-
use std::env;
6+
use sqlx::{Column, PgPool, Row};
7+
use std::sync::LazyLock;
8+
use std::{collections::BTreeMap, env};
9+
10+
static IS_DEV: LazyLock<bool> = LazyLock::new(|| std::env::args().any(|arg| arg == "--dev"));
611

712
async fn index(db_pool: web::Data<PgPool>) -> impl Responder {
813
let row: (String,) = sqlx::query_as("SELECT 'Hello from the database!'")
@@ -13,18 +18,51 @@ async fn index(db_pool: web::Data<PgPool>) -> impl Responder {
1318
HttpResponse::Ok().body(row.0)
1419
}
1520

21+
pub async fn get_all_data(db_pool: web::Data<PgPool>) -> impl Responder {
22+
if !*IS_DEV {
23+
return HttpResponse::Ok().body("This endpoint is not available in production mode.");
24+
}
25+
26+
let tables_query =
27+
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'";
28+
let tables: Vec<(String,)> = sqlx::query_as(tables_query)
29+
.fetch_all(&**db_pool)
30+
.await
31+
.unwrap();
32+
33+
let mut all_data = BTreeMap::new();
34+
35+
for (table_name,) in tables {
36+
let query = format!("SELECT * FROM {}", table_name);
37+
let rows = sqlx::query(&query).fetch_all(&**db_pool).await.unwrap();
38+
39+
let table_data: Vec<BTreeMap<String, Option<String>>> = rows
40+
.iter()
41+
.map(|row| {
42+
let mut map = BTreeMap::new();
43+
for (i, column) in row.columns().iter().enumerate() {
44+
let value: Option<String> = row.try_get(i).unwrap_or(None);
45+
map.insert(column.name().to_string(), value);
46+
}
47+
map
48+
})
49+
.collect();
50+
51+
all_data.insert(table_name, table_data);
52+
}
53+
54+
HttpResponse::Ok().json(all_data)
55+
}
56+
1657
#[actix_web::main]
1758
async fn main() -> std::io::Result<()> {
18-
// Get --dev flag
19-
let is_dev = std::env::args().any(|arg| arg == "--dev");
20-
2159
// Load .env from root
2260
let cwd = env::current_dir().expect("Could not get current directory");
2361
let dotenv_path = cwd.join("../.env");
2462
from_path(&dotenv_path).expect(&format!("Failed to load .env from {:?}", dotenv_path));
2563

2664
// Choose DB connection credentials based on mode
27-
let (host, port, user, password, db) = if is_dev {
65+
let (host, port, user, password, db) = if *IS_DEV {
2866
(
2967
env::var("POSTGRES_DEV_HOST").unwrap(),
3068
env::var("POSTGRES_DEV_PORT").unwrap(),
@@ -53,7 +91,7 @@ async fn main() -> std::io::Result<()> {
5391
// Start the server
5492
println!(
5593
"Running in {} mode",
56-
if is_dev { "development" } else { "production" }
94+
if *IS_DEV { "development" } else { "production" }
5795
);
5896
println!("Server listening on http://127.0.0.1:8080");
5997

@@ -67,6 +105,7 @@ async fn main() -> std::io::Result<()> {
67105
.allow_any_header(),
68106
)
69107
.route("/", web::get().to(index))
108+
.route("/getall", web::get().to(get_all_data))
70109
})
71110
.bind("0.0.0.0:8080")?
72111
.run()

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"typescript": "^5.0.0"
2121
},
2222
"scripts": {
23-
"dev": "bun --watch . --dev",
23+
"dev": "concurrently --names \"WEB,API,BOT\" --prefix-colors \"blue,green,magenta\" \"cd web && bun dev\" \"cd api && cargo watch -x \\\"run -- --dev\\\"\" \"bun --watch . --dev\"",
24+
"dev:bot": "bun --watch . --dev",
2425
"test:jetstream": "bun --watch src/utils/bluesky/jetstream.ts",
2526
"lint": "eslint . --ext .ts -c .eslintrc.json",
2627
"lint:fix": "eslint . --ext .ts -c .eslintrc.json --fix",

src/commands.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
checkIfChannelIsAlreadyTracked,
3232
addNewChannelToTrack,
3333
} from "./utils/db/youtube";
34+
import search from "./utils/youtube/search";
3435

3536
import client from ".";
3637

@@ -488,9 +489,36 @@ const commands: Record<string, Command> = {
488489
const platform = interaction.options.get("platform")?.value;
489490
const query = interaction.options.get("user_id")?.value;
490491

491-
if (!query) {
492+
// If the query is empty or not a string, return an empty array
493+
if (!query || typeof query !== "string") {
494+
await interaction.respond([]);
495+
496+
return;
497+
}
498+
499+
// If the query is a YouTube channel ID, do not search
500+
if (query.length == 24 && query.startsWith("UC")) {
501+
await interaction.respond([
502+
{
503+
name: `${query} (using channel id)`,
504+
value: query,
505+
},
506+
]);
507+
492508
return;
493509
}
510+
511+
const channels = await search(query);
512+
513+
await interaction.respond(
514+
channels.map((channel) => ({
515+
name: `${channel.title} (${channel.handle}) | ${channel.subscribers} subscriber(s)`.slice(
516+
0,
517+
100,
518+
),
519+
value: channel.channel_id,
520+
})),
521+
);
494522
} catch (error) {
495523
console.error(error);
496524
}

src/events/commandHandlerAuto.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Events } from "discord.js";
2+
3+
import client from "..";
4+
import commandsMap from "../commands";
5+
6+
client.on(Events.InteractionCreate, async (interaction) => {
7+
if (interaction.isAutocomplete()) {
8+
const getCommand = commandsMap.get(interaction.commandName);
9+
10+
if (!getCommand?.autoComplete)
11+
return console.log(
12+
`${interaction.user.displayName} tried to do autocomplete for /${interaction.commandName} (${interaction.commandId}) but it wasn't found.`,
13+
);
14+
15+
return getCommand.autoComplete(interaction);
16+
}
17+
});

src/utils/formatLargeNumber.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export default function (input: string | null | undefined): string {
2+
if (!input) return "N/A";
3+
4+
const match = input.match(/^([\d.]+)\s*(K|M|B)?/i);
5+
6+
if (!match) return "N/A";
7+
8+
const [, numericString, suffix] = match;
9+
const numericValue = parseFloat(numericString);
10+
11+
const multipliers: Record<string, number> = {
12+
K: 1e3,
13+
M: 1e6,
14+
B: 1e9,
15+
};
16+
17+
return (
18+
numericValue * (multipliers[suffix?.toUpperCase()] || 1)
19+
).toLocaleString();
20+
}

src/utils/quickEmbed.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
enum EmbedType {
2+
Success = "success",
3+
Error = "error",
4+
Warning = "warning",
5+
Info = "info",
6+
}
7+
8+
export default async function (query: string, type: EmbedType) {
9+
// Wow gonna build an embed no way!
10+
}

src/utils/youtube/search.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// NOTE: Experimental
21
import type { InnertubeSearchRequest } from "../../types/youtube";
2+
import formatLargeNumber from "../formatLargeNumber";
33

44
export default async function (query: string) {
55
try {
@@ -30,7 +30,33 @@ export default async function (query: string) {
3030
.sectionListRenderer.contents;
3131

3232
console.dir(data, { depth: null });
33+
34+
if (!data || data.length === 0) {
35+
console.error("No search results found for query:", query);
36+
return [];
37+
}
38+
39+
const channelsResponse: Array<{
40+
title: string;
41+
handle: string;
42+
subscribers: number | string;
43+
channel_id: string;
44+
}> = [];
45+
for (const content of data ?? []) {
46+
for (const channel of content?.itemSectionRenderer?.contents ?? []) {
47+
if (channel?.channelRenderer?.channelId) {
48+
channelsResponse.push({
49+
title: channel?.channelRenderer?.longBylineText?.runs?.[0]?.text || "N/A",
50+
handle: channel?.channelRenderer?.subscriberCountText?.simpleText || "N/A",
51+
subscribers: formatLargeNumber(channel?.channelRenderer?.videoCountText?.simpleText),
52+
channel_id: channel?.channelRenderer?.channelId || "N/A",
53+
});
54+
}
55+
}
56+
}
57+
return channelsResponse;
3358
} catch (err) {
3459
console.error(err);
60+
return [];
3561
}
3662
}

web/src/components/Navbar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const navbarLinks = [
1111
{ label: "Twitch", href: "/#twitch", ariaLabel: "Twitch" },
1212
{ label: "Discord", href: "/#discord", ariaLabel: "Discord" },
1313
{ label: "FAQ", href: "/#FAQ", ariaLabel: "FAQ" },
14+
{ label: "Stats", href: "/stats", ariaLabel: "Stats" },
1415
];
1516

1617
export const Navbar = () => {

web/src/layouts/Layout.astro

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,36 @@ import "@fontsource/inter/800.css";
77
import "@fontsource/inter/900.css";
88
99
export interface Props {
10-
title: string;
10+
title: string;
1111
}
1212
1313
const { title } = Astro.props;
1414
---
1515

1616
<!doctype html>
1717
<html lang="en" class="bg-bgDark2">
18-
<head>
19-
<meta charset="UTF-8" />
20-
<meta name="viewport" content="width=device-width" />
21-
<link rel="icon" type="image/png" href="/feedr.png" />
22-
<meta name="generator" content={Astro.generator} />
23-
<meta
24-
name="description"
25-
content="Dark themed website template built on AstroJS, designed for fictional startup"
26-
/>
27-
<title>{title}</title>
28-
</head>
29-
<body class="bg-bgDark2">
30-
<main aria-label="Main content">
31-
<slot />
32-
</main>
33-
<style is:global>
34-
html {
35-
font-family: Inter;
36-
background-color: #26272b;
37-
overflow-x: hidden;
38-
scroll-behavior: smooth !important;
39-
}
40-
</style>
41-
</body>
18+
<head>
19+
<meta charset="UTF-8" />
20+
<meta name="viewport" content="width=device-width" />
21+
<link rel="icon" type="image/png" href="/feedr.png" />
22+
<meta name="generator" content={Astro.generator} />
23+
<meta
24+
name="description"
25+
content="Dark themed website template built on AstroJS, designed for fictional startup"
26+
/>
27+
<title>{title}</title>
28+
</head>
29+
<body class="bg-bgDark2">
30+
<main aria-label="Main content">
31+
<slot />
32+
</main>
33+
<style is:global>
34+
html {
35+
font-family: Inter;
36+
background-color: #26272b;
37+
overflow-x: hidden;
38+
scroll-behavior: smooth !important;
39+
}
40+
</style>
41+
</body>
4242
</html>

web/src/pages/stats.astro

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
import Layout from "../layouts/Layout.astro";
3+
import { Hero } from "../components/Hero";
4+
import { Navbar } from "../components/Navbar";
5+
// import { Features1 } from "../components/Features1";
6+
// import { Features2 } from "../components/Features2";
7+
// import { Testimonials } from "../components/Testimonials";
8+
import { FeaturesDiagonal } from "../components/FeaturesDiagonal";
9+
import { FAQ } from "../components/FAQ";
10+
import { TrustedBy } from "../components/TrustedBy";
11+
import { Footer } from "../components/Footer";
12+
import { ScrollUpButton } from "../components/ScrollUpButton";
13+
import "../styles/Theme.css";
14+
import "../styles/Diagonals.css";
15+
// import { DiscordMessageEmbed } from "../components/DiscordMessageEmbed";
16+
import { FeaturesYouTube } from "../components/FeaturesYouTube";
17+
---
18+
19+
<Layout title="Feedr Discord Bot">
20+
<Navbar client:load />
21+
<Hero client:load />
22+
<!-- <DiscordMessageEmbed client:load /> -->
23+
<!-- <Features1 client:load />
24+
<Features2 client:load /> -->
25+
<FeaturesYouTube client:load />
26+
<FeaturesDiagonal client:load />
27+
<!-- <Testimonials client:load /> -->
28+
<TrustedBy client:load />
29+
<FAQ client:load />
30+
<Footer />
31+
<ScrollUpButton client:load />
32+
</Layout>

0 commit comments

Comments
 (0)