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 { zValidator } from "@hono/zod-validator";
|
||||||
import innertube from "../lib/innertube.js";
|
import innertube from "../lib/innertube.js";
|
||||||
import { PlayVideo } from "../models/PlayVideo.js";
|
import { VideoFindSchema } from "../models/VideoFindSchema.js";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
|
|
||||||
const playback = new Hono();
|
const playback = new Hono();
|
||||||
|
|
||||||
playback.get(
|
playback.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
zValidator("param", PlayVideo),
|
zValidator("param", VideoFindSchema),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { id } = c.req.valid("param");
|
const { id } = c.req.valid("param");
|
||||||
const data = await innertube.getStreamingData(id, {
|
const data = await innertube.getStreamingData(id, {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import innertube from "../lib/innertube.js";
|
import innertube from "../lib/innertube.js";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
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 search from "../templates/search.js";
|
||||||
import { YTNodes } from "youtubei.js";
|
import { YTNodes } from "youtubei.js";
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ const video = new Hono();
|
|||||||
|
|
||||||
video.get(
|
video.get(
|
||||||
"/videos",
|
"/videos",
|
||||||
zValidator("query", SearchVideos),
|
zValidator("query", VideoSearchSchema),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const params = c.req.valid("query");
|
const params = c.req.valid("query");
|
||||||
const page = params["start-index"] ?? 1;
|
const page = params["start-index"] ?? 1;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { config } from "./lib/config.js"
|
|||||||
|
|
||||||
import video from "./api/video.js"
|
import video from "./api/video.js"
|
||||||
import playback from "./api/playback.js"
|
import playback from "./api/playback.js"
|
||||||
|
import channel from "./api/channel.js"
|
||||||
|
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ app.use("/schemas/*", serveStatic({ root: "./" }))
|
|||||||
|
|
||||||
app.route("/getvideo", playback);
|
app.route("/getvideo", playback);
|
||||||
app.route("/feeds/api", video);
|
app.route("/feeds/api", video);
|
||||||
|
app.route("/feeds/api", channel);
|
||||||
|
|
||||||
serve({
|
serve({
|
||||||
fetch: app.fetch,
|
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";
|
import z from "zod";
|
||||||
|
|
||||||
export const PlayVideo = z.object({
|
export const VideoFindSchema = z.object({
|
||||||
id: z.string().length(11)
|
id: z.string().length(11)
|
||||||
});
|
});
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
export const SearchVideos = z.object({
|
export const VideoSearchSchema = z.object({
|
||||||
q: z.string().min(1),
|
q: z.string().min(1),
|
||||||
time: z.preprocess((val) => {
|
time: z.preprocess((val) => {
|
||||||
if (typeof val !== "string") return undefined;
|
if (typeof val !== "string") return undefined;
|
||||||
@ -17,9 +17,10 @@ export const SearchVideos = z.object({
|
|||||||
if (typeof val !== "string") return undefined;
|
if (typeof val !== "string") return undefined;
|
||||||
switch (val) {
|
switch (val) {
|
||||||
case "rating": return "rating";
|
case "rating": return "rating";
|
||||||
case "published": "upload_date;"
|
case "published": return "upload_date;"
|
||||||
case "viewCount": return "view_count";
|
case "viewCount": return "view_count";
|
||||||
default: return "relevance";
|
case "relevance": return "relevance";
|
||||||
|
default: return undefined;
|
||||||
}
|
}
|
||||||
}, z.enum(["relevance", "rating", "upload_date", "view_count"]).optional()),
|
}, z.enum(["relevance", "rating", "upload_date", "view_count"]).optional()),
|
||||||
duration: z.enum(['all', 'short', 'medium', 'long']).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