diff --git a/src/api/channel.ts b/src/api/channel.ts new file mode 100644 index 0000000..c132c50 --- /dev/null +++ b/src/api/channel.ts @@ -0,0 +1,65 @@ +import { zValidator } from "@hono/zod-validator"; +import innertube from "../lib/innertube.js"; +import { ChannelFindSchema } from "../models/ChannelFindSchema.js"; +import { Hono } from "hono"; +import channelData from "../templates/channelData.js"; +import { parseSubscriberCount } from "../lib/channel.js"; +import { YTNodes } from "youtubei.js"; + +const channel = new Hono(); + +channel.get( + "/channels/:id", + zValidator("param", ChannelFindSchema), + async (c) => { + const { id } = c.req.valid("param"); + + try { + const data = await innertube.getChannel(id); + const { header } = data; + + if (!header) { + return c.text("Channel not found", 404); + } + + const channelInfo = header.is(YTNodes.PageHeader) + ? { + name: header.page_title, + description: header.content?.description?.description?.text || "", + thumbnail: (header.content?.image as YTNodes.DecoratedAvatarView) + ?.avatar?.image?.[0]?.url || "", + subCount: parseSubscriberCount( + header.content?.metadata?.metadata_rows?.[1] + ?.metadata_parts?.[0]?.text?.text || "0" + ), + } + : header.is(YTNodes.C4TabbedHeader) + ? { + name: header.author?.name || "", + description: data.metadata?.description || "", + thumbnail: header.author?.best_thumbnail?.url || "", + subCount: parseSubscriberCount(header.subscribers?.text || "0"), + } + : null; + + if (!channelInfo) { + return c.text("Channel not found", 404); + } + + return c.html( + channelData( + id, + channelInfo.name, + channelInfo.subCount, + channelInfo.description, + channelInfo.thumbnail + ) + ); + } catch (err) { + console.error("Error fetching channel:", err); + return c.text("Channel not found", 404); + } + } +); + +export default channel \ No newline at end of file diff --git a/src/api/playback.ts b/src/api/playback.ts index 055859d..f1c30f7 100644 --- a/src/api/playback.ts +++ b/src/api/playback.ts @@ -1,13 +1,13 @@ import { zValidator } from "@hono/zod-validator"; import innertube from "../lib/innertube.js"; -import { PlayVideo } from "../models/PlayVideo.js"; +import { VideoFindSchema } from "../models/VideoFindSchema.js"; import { Hono } from "hono"; const playback = new Hono(); playback.get( "/:id", - zValidator("param", PlayVideo), + zValidator("param", VideoFindSchema), async (c) => { const { id } = c.req.valid("param"); const data = await innertube.getStreamingData(id, { diff --git a/src/api/video.ts b/src/api/video.ts index aa01e6f..1eb481c 100644 --- a/src/api/video.ts +++ b/src/api/video.ts @@ -1,7 +1,7 @@ import { Hono } from "hono"; import innertube from "../lib/innertube.js"; import { zValidator } from "@hono/zod-validator"; -import { SearchVideos } from "../models/SearchVideos.js"; +import { VideoSearchSchema } from "../models/VideoSearchSchema.js"; import search from "../templates/search.js"; import { YTNodes } from "youtubei.js"; @@ -9,7 +9,7 @@ const video = new Hono(); video.get( "/videos", - zValidator("query", SearchVideos), + zValidator("query", VideoSearchSchema), async (c) => { const params = c.req.valid("query"); const page = params["start-index"] ?? 1; diff --git a/src/index.ts b/src/index.ts index 484d3d4..e3da8f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { config } from "./lib/config.js" import video from "./api/video.js" import playback from "./api/playback.js" +import channel from "./api/channel.js" const app = new Hono() @@ -15,6 +16,7 @@ app.use("/schemas/*", serveStatic({ root: "./" })) app.route("/getvideo", playback); app.route("/feeds/api", video); +app.route("/feeds/api", channel); serve({ fetch: app.fetch, diff --git a/src/lib/channel.ts b/src/lib/channel.ts new file mode 100644 index 0000000..a166521 --- /dev/null +++ b/src/lib/channel.ts @@ -0,0 +1,16 @@ +export function parseSubscriberCount(text: string) { + const cleaned = text.replace(/[^\d.KMB]/gi, ""); + const multipliers: Record = { + K: 1_000, + M: 1_000_000, + B: 1_000_000_000, + }; + + for (const [suffix, multiplier] of Object.entries(multipliers)) { + if (cleaned.toUpperCase().includes(suffix)) { + return Math.floor(parseFloat(cleaned) * multiplier); + } + } + + return parseInt(cleaned) || 0; +} \ No newline at end of file diff --git a/src/models/ChannelFindSchema.ts b/src/models/ChannelFindSchema.ts new file mode 100644 index 0000000..8dcb55e --- /dev/null +++ b/src/models/ChannelFindSchema.ts @@ -0,0 +1,5 @@ +import z from "zod"; + +export const ChannelFindSchema = z.object({ + id: z.string().length(24) +}); \ No newline at end of file diff --git a/src/models/PlayVideo.ts b/src/models/VideoFindSchema.ts similarity index 52% rename from src/models/PlayVideo.ts rename to src/models/VideoFindSchema.ts index a57ca93..47bfa8c 100644 --- a/src/models/PlayVideo.ts +++ b/src/models/VideoFindSchema.ts @@ -1,5 +1,5 @@ import z from "zod"; -export const PlayVideo = z.object({ +export const VideoFindSchema = z.object({ id: z.string().length(11) }); \ No newline at end of file diff --git a/src/models/SearchVideos.ts b/src/models/VideoSearchSchema.ts similarity index 83% rename from src/models/SearchVideos.ts rename to src/models/VideoSearchSchema.ts index 6f85be8..a994249 100644 --- a/src/models/SearchVideos.ts +++ b/src/models/VideoSearchSchema.ts @@ -1,6 +1,6 @@ import z from "zod"; -export const SearchVideos = z.object({ +export const VideoSearchSchema = z.object({ q: z.string().min(1), time: z.preprocess((val) => { if (typeof val !== "string") return undefined; @@ -17,9 +17,10 @@ export const SearchVideos = z.object({ if (typeof val !== "string") return undefined; switch (val) { case "rating": return "rating"; - case "published": "upload_date;" + case "published": return "upload_date;" case "viewCount": return "view_count"; - default: return "relevance"; + case "relevance": return "relevance"; + default: return undefined; } }, z.enum(["relevance", "rating", "upload_date", "view_count"]).optional()), duration: z.enum(['all', 'short', 'medium', 'long']).optional(), diff --git a/src/templates/channelData.ts b/src/templates/channelData.ts new file mode 100644 index 0000000..aafed92 --- /dev/null +++ b/src/templates/channelData.ts @@ -0,0 +1,29 @@ +import { html } from "hono/html"; + +export default ( + id: string, + name: string, + subCount: number, + description: string, + pictureUrl: string +) => html` + + tag:youtube.com,2008:channel:${id} + 2014-09-16T18:07:05.000Z + + ${name} + ${description} + + + + + ${name} + /feeds/api/users/webauditors + ${id} + + ${id} + + + + +` \ No newline at end of file