add channel data
This commit is contained in:
parent
3355a51b74
commit
cc3309d062
65
src/api/channel.ts
Normal file
65
src/api/channel.ts
Normal file
@ -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
|
||||
@ -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, {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
16
src/lib/channel.ts
Normal file
16
src/lib/channel.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export function parseSubscriberCount(text: string) {
|
||||
const cleaned = text.replace(/[^\d.KMB]/gi, "");
|
||||
const multipliers: Record<string, number> = {
|
||||
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;
|
||||
}
|
||||
5
src/models/ChannelFindSchema.ts
Normal file
5
src/models/ChannelFindSchema.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import z from "zod";
|
||||
|
||||
export const ChannelFindSchema = z.object({
|
||||
id: z.string().length(24)
|
||||
});
|
||||
@ -1,5 +1,5 @@
|
||||
import z from "zod";
|
||||
|
||||
export const PlayVideo = z.object({
|
||||
export const VideoFindSchema = z.object({
|
||||
id: z.string().length(11)
|
||||
});
|
||||
@ -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(),
|
||||
29
src/templates/channelData.ts
Normal file
29
src/templates/channelData.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { html } from "hono/html";
|
||||
|
||||
export default (
|
||||
id: string,
|
||||
name: string,
|
||||
subCount: number,
|
||||
description: string,
|
||||
pictureUrl: string
|
||||
) => html`
|
||||
<entry gd:etag='W/"Ck8GRH47eCp7I2A9XRdTGEQ."'>
|
||||
<id>tag:youtube.com,2008:channel:${id}</id>
|
||||
<updated>2014-09-16T18:07:05.000Z</updated>
|
||||
<category scheme='http://schemas.google.com/g/2005#kind' term='/schemas/2007#channel'/>
|
||||
<title>${name}</title>
|
||||
<summary>${description}</summary>
|
||||
<link rel='/schemas/2007#featured-video' type='application/atom+xml' href='/feeds/api/videos/YM582qGZHLI?v=2'/>
|
||||
<link rel='alternate' type='text/html' href='https://www.youtube.com/channel/${id}'/>
|
||||
<link rel='self' type='application/atom+xml' href='/feeds/api/channels/${id}?v=2'/>
|
||||
<author>
|
||||
<name>${name}</name>
|
||||
<uri>/feeds/api/users/webauditors</uri>
|
||||
<yt:userId>${id}</yt:userId>
|
||||
</author>
|
||||
<yt:channelId>${id}</yt:channelId>
|
||||
<yt:channelStatistics subscriberCount='${subCount}' viewCount='0'/>
|
||||
<gd:feedLink rel='/schemas/2007#channel.content' href='/feeds/api/users/webauditors/uploads?v=2' countHint='0'/>
|
||||
<media:thumbnail url='${pictureUrl}'/>
|
||||
</entry>
|
||||
`
|
||||
Loading…
x
Reference in New Issue
Block a user