This commit is contained in:
murdle 2025-12-23 02:14:20 +02:00
parent cc3309d062
commit c82b1ac6b0
17 changed files with 490 additions and 153 deletions

3
.gitignore vendored
View File

@ -26,3 +26,6 @@ lerna-debug.log*
# misc
.DS_Store
# cache
cache/

View File

@ -4,5 +4,5 @@ npm run dev
```
```
open http://localhost:3000
open http://localhost:4000
```

View File

@ -1,10 +1,9 @@
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";
import { ChannelFindSchema } from "../models/ChannelFindSchema.js";
import { parseChannel } from "../lib/utils/channel.js";
import { Hono } from "hono";
const channel = new Hono();
@ -16,45 +15,13 @@ channel.get(
try {
const data = await innertube.getChannel(id);
const { header } = data;
const info = parseChannel(data);
if (!header) {
if (!info) {
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
)
);
return c.html(channelData(info));
} catch (err) {
console.error("Error fetching channel:", err);
return c.text("Channel not found", 404);

25
src/api/feed.ts Normal file
View File

@ -0,0 +1,25 @@
import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import innertube from "../lib/innertube.js";
import homeFeed from "../templates/homeFeed.js";
import { FeedGetSchema } from "../models/FeedGetSchema.js";
import { parseHomeFeed } from "../lib/utils/feed.js";
import type { YT } from "youtubei.js";
const feed = new Hono();
feed.get(
"/standardfeeds/:region/:type",
zValidator("param", FeedGetSchema),
async (c) => {
const { type } = c.req.valid("param");
if (type != "most_popular") return c.html(homeFeed())
const data = await innertube.getHomeFeed();
const feed = data as YT.HomeFeed;
return c.html(homeFeed(await parseHomeFeed(feed)))
}
)
export default feed

View File

@ -3,6 +3,7 @@ import innertube from "../lib/innertube.js";
import { zValidator } from "@hono/zod-validator";
import { VideoSearchSchema } from "../models/VideoSearchSchema.js";
import search from "../templates/search.js";
import { parseVideos } from "../lib/utils/video.js";
import { YTNodes } from "youtubei.js";
const video = new Hono();
@ -12,43 +13,47 @@ video.get(
zValidator("query", VideoSearchSchema),
async (c) => {
const params = c.req.valid("query");
const page = params["start-index"] ?? 1;
const page = Number(params["start-index"] ?? 1);
if (page > 5) {
return c.html(search([], ""));
}
// * fetch all videos up to current page
const allVideos: YTNodes.Video[] = [];
let searchQuery = await innertube.search(params.q, {
type: "video",
duration: params.duration,
upload_date: params.time,
sort_by: params.orderby
});
for (let i = 1; i <= page; i++) {
let currentPage = 1;
while (currentPage <= page) {
const videos = searchQuery.results.filter(
(node): node is YTNodes.Video => node.type === "Video"
);
allVideos.push(...videos);
if (i < page) {
try {
const nextPage = await searchQuery.getContinuation();
if (!nextPage) break;
searchQuery = nextPage;
} catch (err) {
// * no more pages available
break;
}
}
if (!searchQuery.has_continuation) break;
if (currentPage === page) break;
searchQuery = await searchQuery.getContinuation();
currentPage++;
}
// * build next page URL
const nextUrl = new URL(c.req.url);
nextUrl.searchParams.set("start-index", (page + 1).toString());
nextUrl.searchParams.set("start-index", String(page + 1));
return c.html(search(allVideos, nextUrl.toString()));
return c.html(
search(
parseVideos(allVideos),
searchQuery.has_continuation ? nextUrl.toString() : ""
)
);
}
);

View File

@ -8,6 +8,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"
import feed from "./api/feed.js"
const app = new Hono()
@ -17,6 +18,7 @@ app.use("/schemas/*", serveStatic({ root: "./" }))
app.route("/getvideo", playback);
app.route("/feeds/api", video);
app.route("/feeds/api", channel);
app.route("/feeds/api", feed);
serve({
fetch: app.fetch,

View File

@ -1,16 +0,0 @@
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;
}

View File

@ -1,12 +1,25 @@
import { z } from "zod";
import "dotenv/config"
import fs from "fs";
import path from "path";
const cookiesPath = path.resolve(process.cwd(), "cookies.txt");
let cookies: string | null = null;
if (fs.existsSync(cookiesPath)) {
cookies = fs.readFileSync(cookiesPath, "utf8").trim();
}
const EnvSchema = z.object({
BASE_URL: z.url(),
PORT: z.coerce.number().default(4000)
BASE_URL: z.string().url(),
PORT: z.coerce.number().default(4000),
COOKIES: z.string().nullable().default(null),
});
const parsed = EnvSchema.safeParse(process.env);
const parsed = EnvSchema.safeParse({
...process.env,
COOKIES: cookies ?? null,
});
if (!parsed.success) {
console.error("Invalid config", parsed.error.format());

View File

@ -1,4 +1,5 @@
import { Innertube, Platform, type Types } from 'youtubei.js';
import { ClientType, Innertube, Platform, type Types, UniversalCache } from 'youtubei.js';
import { config } from './config.js';
Platform.shim.eval = async (
data: Types.BuildScriptResult,
@ -12,5 +13,11 @@ Platform.shim.eval = async (
return new Function(code)();
};
const innertube = await Innertube.create();
const innertube = await Innertube.create({
cache: new UniversalCache(true, "./cache"),
device_category: "mobile",
enable_session_cache: true,
client_type: ClientType.WEB,
...(config.COOKIES ? { cookie: config.COOKIES } : {})
});
export default innertube

66
src/lib/utils.ts Normal file
View File

@ -0,0 +1,66 @@
export function parseCount(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;
}
export function parseDuration(duration: string) {
const parts = duration.split(":").map(Number);
if (parts.length === 3) {
return parts[0] * 3600 + parts[1] * 60 + parts[2];
} else if (parts.length === 2) {
return parts[0] * 60 + parts[1];
} else if (parts.length === 1) {
return parts[0];
}
return 0;
}
export function parseRelativeDate(text: string): string | null {
const now = new Date();
const match = text.match(/(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago/);
if (!match) return null;
const unit = match[2];
const date = new Date(now);
const value = parseInt(match[1]);
switch (unit) {
case "second":
date.setSeconds(date.getSeconds() - value);
break;
case "minute":
date.setMinutes(date.getMinutes() - value);
break;
case "hour":
date.setHours(date.getHours() - value);
break;
case "day":
date.setDate(date.getDate() - value);
break;
case "week":
date.setDate(date.getDate() - value * 7);
break;
case "month":
date.setMonth(date.getMonth() - value);
break;
case "year":
date.setFullYear(date.getFullYear() - value);
break;
}
return date.toISOString();
}

42
src/lib/utils/channel.ts Normal file
View File

@ -0,0 +1,42 @@
import { YTNodes, YT } from "youtubei.js";
import { parseCount } from "../utils.js";
export interface Channel {
id: string;
name: string;
description: string;
thumbnail: string;
subCount: number;
}
// TODO: make this not ugly
export function parseChannel(data: YT.Channel): Channel | null {
const { header, metadata } = data;
if (!header) return null;
if (header.is(YTNodes.PageHeader)) {
return {
id: metadata?.external_id || "",
name: header.page_title,
description: header.content?.description?.description?.text || "",
thumbnail: (header.content?.image as YTNodes.DecoratedAvatarView)
?.avatar?.image?.[0]?.url || "",
subCount: parseCount(
header.content?.metadata?.metadata_rows?.[1]
?.metadata_parts?.[0]?.text?.text || "0"
),
};
}
if (header.is(YTNodes.C4TabbedHeader)) {
return {
id: metadata?.external_id || "",
name: header.author?.name || "",
description: data.metadata?.description || "",
thumbnail: header.author?.best_thumbnail?.url || "",
subCount: parseCount(header.subscribers?.text || "0"),
};
}
return null;
}

74
src/lib/utils/feed.ts Normal file
View File

@ -0,0 +1,74 @@
import { YTNodes } from "youtubei.js";
import { parseCount, parseDuration, parseRelativeDate } from "../utils.js";
import { type Video } from "./video.js";
import { YT } from "youtubei.js";
export function parseFeedVideo(
video: YTNodes.Video | YTNodes.RichItem
): Video | null {
const content = video.is(YTNodes.RichItem) ? video.content : video;
if (!content || content.type !== "LockupView") return null;
const lockup = content as YTNodes.LockupView;
if (lockup.content_type !== "VIDEO") return null;
const thumbnail = lockup.content_image as YTNodes.ThumbnailView;
const metadataRows = lockup.metadata?.metadata?.metadata_rows ?? [];
// * extract author info
const authorPart = metadataRows[0]?.metadata_parts?.[0]?.text;
const authorName = authorPart?.text ?? "";
const authorId = authorPart?.endpoint?.payload?.browseId ?? "";
// * extract view count and published date
const viewText = metadataRows[1]?.metadata_parts?.[0]?.text?.text ?? "0";
const createdText = metadataRows[1]?.metadata_parts?.[1]?.text?.text ?? "0 seconds ago";
// * extract video duration
const overlay = thumbnail?.overlays[0] as YTNodes.ThumbnailOverlayBadgeView;
const durationText = overlay ? overlay.badges[0]?.text : undefined;
return {
id: lockup.content_id ?? "",
title: lockup.metadata?.title?.text ?? "",
description: "",
thumbnailUrl: thumbnail?.image?.[0]?.url ?? "",
authorName,
authorId,
viewCount: parseCount(viewText),
created: parseRelativeDate(createdText) ?? "",
duration: parseDuration(durationText ?? "0:00"),
};
}
export async function parseHomeFeed(feed: YT.HomeFeed): Promise<Video[]> {
const videos: Video[] = [];
let currentFeed = feed;
while (currentFeed.has_continuation) {
const grid = currentFeed.contents;
if (!grid || !grid.is(YTNodes.RichGrid)) break;
for (const item of grid.contents) {
if (item.is(YTNodes.RichItem)) {
const parsed = parseFeedVideo(item);
if (parsed) videos.push(parsed);
} else if (item.is(YTNodes.RichSection)) {
const sectionContent = item.content;
if (sectionContent?.is(YTNodes.RichShelf)) {
for (const shelfItem of sectionContent.contents || []) {
if (shelfItem.is(YTNodes.RichItem)) {
const parsed = parseFeedVideo(shelfItem);
if (parsed) videos.push(parsed);
}
}
}
}
}
const continuation = await currentFeed.getContinuation();
currentFeed = continuation;
}
return videos;
}

40
src/lib/utils/video.ts Normal file
View File

@ -0,0 +1,40 @@
import { YTNodes } from "youtubei.js";
import { parseCount, parseDuration, parseRelativeDate } from "../utils.js";
export interface Video {
id: string;
title: string;
description: string;
authorId: string;
authorName: string;
duration: number;
viewCount: number;
thumbnailUrl: string;
created: string;
}
export function parseVideos(videos: YTNodes.Video[]): Video[] {
return videos.map(video => parseVideo(video));
}
export function parseVideo(video: YTNodes.Video): Video {
const authorId = video.author?.id && video.author.id !== "N/A"
? video.author.id
: video.video_id;
const authorName = video.author?.name && video.author.name !== "N/A"
? video.author.name
: "Unknown";
return {
id: video.video_id,
title: video.title?.text || "",
description: video.description || "",
authorId,
authorName,
duration: parseDuration(video.duration?.text || "0:00"),
viewCount: parseCount(video.view_count?.text || "0"),
thumbnailUrl: video.thumbnails?.[0]?.url || `https://i.ytimg.com/vi/${video.video_id}/default.jpg`,
created: parseRelativeDate(video?.published?.text || "0 seconds ago") ?? ""
};
}

View File

@ -0,0 +1,6 @@
import z from "zod";
export const FeedGetSchema = z.object({
region: z.string().length(2),
type: z.enum(["most_popular", "most_popular_Film", "most_popular_Games", "most_popular_Music"])
});

View File

@ -1,29 +1,26 @@
import { html } from "hono/html";
import type { Channel } from "../lib/utils/channel.js";
export default (
id: string,
name: string,
subCount: number,
description: string,
pictureUrl: string
channel: Channel
) => html`
<entry gd:etag='W/&quot;Ck8GRH47eCp7I2A9XRdTGEQ.&quot;'>
<id>tag:youtube.com,2008:channel:${id}</id>
<id>tag:youtube.com,2008:channel:${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>
<title>${channel.name}</title>
<summary>${channel.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'/>
<link rel='alternate' type='text/html' href='https://www.youtube.com/channel/${channel.id}'/>
<link rel='self' type='application/atom+xml' href='/feeds/api/channels/${channel.id}?v=2'/>
<author>
<name>${name}</name>
<name>${channel.name}</name>
<uri>/feeds/api/users/webauditors</uri>
<yt:userId>${id}</yt:userId>
<yt:userId>${channel.id}</yt:userId>
</author>
<yt:channelId>${id}</yt:channelId>
<yt:channelStatistics subscriberCount='${subCount}' viewCount='0'/>
<yt:channelId>${channel.id}</yt:channelId>
<yt:channelStatistics subscriberCount='${channel.subCount}' viewCount='0'/>
<gd:feedLink rel='/schemas/2007#channel.content' href='/feeds/api/users/webauditors/uploads?v=2' countHint='0'/>
<media:thumbnail url='${pictureUrl}'/>
<media:thumbnail url='${channel.thumbnail}'/>
</entry>
`

106
src/templates/homeFeed.ts Normal file
View File

@ -0,0 +1,106 @@
import { html } from "hono/html";
import { config } from "../lib/config.js";
import type { Video } from "../lib/utils/video.js";
export default (
videos: Video[] = []
) => html `
<?xml version='1.0' encoding='UTF-8'?>
<feed
xmlns='http://www.w3.org/2005/Atom'
xmlns:media='http://search.yahoo.com/mrss/'
xmlns:openSearch='http://a9.com/-/spec/opensearch/1.1/'
xmlns:gd='http://schemas.google.com/g/2005'
xmlns:yt='${config.BASE_URL}/schemas/1970' gd:etag='W/&quot;D0UHSX47eCp7I2A9WhJWF08.&quot;'>
<id>tag:youtube.com,2008:playlist:8E2186857EE27746</id>
<updated>2012-08-23T12:33:58.000Z</updated>
<category scheme='http://schemas.google.com/g/2005#kind' term='${config.BASE_URL}/schemas/1970#playlist'/>
<title></title>
<subtitle></subtitle>
<logo>http://www.gstatic.com/youtube/img/logo.png</logo>
<link rel='alternate' type='text/html' href='http://www.youtube.com/playlist?list=PL8E2186857EE27746'/>
<link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='${config.BASE_URL}/feeds/api/playlists/8E2186857EE27746?v=2'/>
<link rel='http://schemas.google.com/g/2005#batch' type='application/atom+xml' href='${config.BASE_URL}/feeds/api/playlists/8E2186857EE27746/batch?v=2'/>
<link rel='self' type='application/atom+xml' href='${config.BASE_URL}/feeds/api/playlists/8E2186857EE27746?start-index=1&amp;max-results=25&amp;v=2'/>
<link rel='service' type='application/atomsvc+xml' href='${config.BASE_URL}/feeds/api/playlists/8E2186857EE27746?alt=atom-service&amp;v=2'/>
<author>
<name></name>
<uri></uri>
<yt:userId></yt:userId>
</author>
<generator version='2.1' uri='${config.BASE_URL}/'>YouTube data API</generator>
<openSearch:totalResults>50</openSearch:totalResults>
<openSearch:itemsPerPage>50</openSearch:itemsPerPage>
<openSearch:startIndex>1</openSearch:startIndex>
${videos.map(video => html`
<entry gd:etag='W/&quot;${video.id}.&quot;'>
<id>tag:youtube.com,2008:playlist:${video.id}:${video.id}</id>
<published>${video.created}</published>
<updated>${video.created}</updated>
<category scheme='http://schemas.google.com/g/2005#kind' term='${config.BASE_URL}/schemas/1970#video'/>
<category scheme='${config.BASE_URL}/schemas/1970/categories.cat' term='Howto' label='Howto &amp; Style'/>
<title>${video.title}</title>
<content type='application/x-shockwave-flash' src=''/>
<link rel='alternate' type='text/html' href="http://www.youtube.com/watch?v=${video.id}&amp;feature=youtube_gdata"/>
<link rel='http://gdata.youtube.com/schemas/2007#video.related' type='application/atom+xml' href="https://gdata.youtube.com/feeds/api/videos/${video.id}/related"/>
<link rel='${config.BASE_URL}/schemas/1970#mobile' type='text/html' href="http://m.youtube.com/details?v=${video.id}"/>
<link rel='${config.BASE_URL}/schemas/1970#uploader' type='application/atom+xml' href="${config.BASE_URL}/feeds/api/users/{${video.authorId}?v=2e"/>
<link rel="${config.BASE_URL}/schemas/2007#related" type="application/atom+xml" href="${config.BASE_URL}/feeds/api/videos/${video.id}/related" />
<link rel='related' type='application/atom+xml' href="${config.BASE_URL}/feeds/api/videos/${video.id}?v=2"/>
<link rel='self' type='application/atom+xml' href='${config.BASE_URL}/feeds/api/playlists/8E2186857EE27746/PLyl9mKRbpNIpJC5B8qpcgKX8v8NI62Jho?v=2'/>
<author>
<name>${video.authorName}</name>
<uri>${config.BASE_URL}/</uri>
<yt:userId>{${video.authorId}</yt:userId>
</author>
<yt:accessControl action='comment' permission='allowed'/>
<yt:accessControl action='commentVote' permission='allowed'/>
<yt:accessControl action='videoRespond' permission='moderated'/>
<yt:accessControl action='rate' permission='allowed'/>
<yt:accessControl action='embed' permission='allowed'/>
<yt:accessControl action='list' permission='allowed'/>
<yt:accessControl action='autoPlay' permission='allowed'/>
<yt:accessControl action='syndicate' permission='allowed'/>
<gd:comments>
<gd:feedLink rel='${config.BASE_URL}/schemas/1970#comments' href="${config.BASE_URL}/api/videos/${video.id}/comments?v=2" countHint='5'/>
</gd:comments>
<gd:related>
<gd:feedLink rel='${config.BASE_URL}/schemas/1970#related' href="${config.BASE_URL}/feeds/api/videos/${video.id}/related" countHint='5'/>
</gd:related>
<yt:location></yt:location>
<media:group>
<media:category label='Howto &amp; Style' scheme='${config.BASE_URL}/schemas/1970/categories.cat'>Howto</media:category>
<media:content url="http://www.youtube.com/v/${video.id}?version=3&amp;f=playlists&amp;app=youtube_gdata" type='application/x-shockwave-flash' medium='video' isDefault='true' expression='full' duration='0' yt:format='5'/>
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type='video/3gpp' type='video/3gpp' medium='video' expression='full' duration='0' yt:format='1'/>
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type='video/3gpp' type='video/3gpp' medium='video' expression='full' duration='0' yt:format='6'/>
<media:credit role='uploader' scheme='urn:youtube' yt:display='${video.authorName}' yt:type='partner'>{${video.authorId}</media:credit>
<media:description type='plain'>${video.description}</media:description>
<media:keywords/>
<media:license type='text/html' href='http://www.youtube.com/t/terms'>youtube</media:license>
<media:player url="http://www.youtube.com/watch?v=${video.id}&amp;feature=youtube_gdata_player"/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='default'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/mqdefault.jpg" height='180' width='320' yt:name='mqdefault'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/hqdefault.jpg" height='360' width='480' yt:name='hqdefault'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='start'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='middle'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='end'/>
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type="video/mp4" medium="video" isDefault="true" expression="full" duration="0" yt:format="3" />
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type="video/3gpp" medium="video" expression="full" duration="0" yt:format="2" />
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type="video/mp4" medium="video" expression="full" duration="0" yt:format="8" />
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type="video/3gpp" medium="video" expression="full" duration="0" yt:format="9" />
<media:title type='plain'>${video.description}}</media:title>
<yt:duration seconds="${video.duration}"/>
<yt:uploaded>${video.created}</yt:uploaded>
<yt:uploaderId>${video.authorId}</yt:uploaderId>
<yt:videoid>${video.id}</yt:videoid>
</media:group>
<gd:rating average='0' max='0' min='0' numRaters='0' rel='https://schemas.google.com/g/2005#overall'/>
<yt:recorded>1970-08-22</yt:recorded>
<yt:statistics favoriteCount='0' viewCount="${video.viewCount}"/>
<yt:rating numDislikes='0' numLikes='0'/>
<yt:position>1</yt:position>
</entry>
`)}
</feed>
`

View File

@ -1,9 +1,9 @@
import { html } from "hono/html";
import { config } from "../lib/config.js";
import { YTNodes } from "youtubei.js";
import type { Video } from "../lib/utils/video.js";
export default (
videos: YTNodes.Video[],
videos: Video[],
nextPage: string
) => html`
<?xml version="1.0" encoding="UTF-8"?>
@ -40,23 +40,23 @@ export default (
${videos.map(video => html`
<entry gd:etag='W/&quot;YDwqeyM.&quot;'>
<id>tag:youtube.com,2008:playlist:${video.video_id}:${video.video_id}</id>
<published>${new Date().toISOString()}</published>
<updated>${new Date().toISOString()}</updated>
<id>tag:youtube.com,2008:playlist:${video.id}:${video.id}</id>
<published>${video.created}</published>
<updated>${video.created}</updated>
<category scheme='https://schemas.google.com/g/2005#kind' term='https://gdata.youtube.com/schemas/1970#video'/>
<category scheme='https://gdata.youtube.com/schemas/1970/categories.cat' term='Howto' label='Howto &amp; Style'/>
<title>${video.title}</title>
<content type='application/x-shockwave-flash' src="https://www.youtube.com/v/${video.video_id}?version=3&amp;f=playlists&amp;app=youtube_gdata"/>
<link rel='alternate' type='text/html' href="https://www.youtube.com/watch?v=${video.video_id}&amp;feature=youtube_gdata"/>
<link rel='http://gdata.youtube.com/schemas/2007#video.related' type='application/atom+xml' href="https://gdata.youtube.com/feeds/api/videos/${video.video_id}/related"/>
<link rel='https://gdata.youtube.com/schemas/1970#mobile' type='text/html' href="https://m.youtube.com/details?v=${video.video_id}"/>
<link rel='https://gdata.youtube.com/schemas/1970#uploader' type='application/atom+xml' href="https://gdata.youtube.com/feeds/api/users/${video.author.id}?v=2"/>
<link rel='related' type='application/atom+xml' href="https://gdata.youtube.com/feeds/api/videos/${video.video_id}?v=2"/>
<content type='application/x-shockwave-flash' src="https://www.youtube.com/v/${video.id}?version=3&amp;f=playlists&amp;app=youtube_gdata"/>
<link rel='alternate' type='text/html' href="https://www.youtube.com/watch?v=${video.id}&amp;feature=youtube_gdata"/>
<link rel='http://gdata.youtube.com/schemas/2007#video.related' type='application/atom+xml' href="https://gdata.youtube.com/feeds/api/videos/${video.id}/related"/>
<link rel='https://gdata.youtube.com/schemas/1970#mobile' type='text/html' href="https://m.youtube.com/details?v=${video.id}"/>
<link rel='https://gdata.youtube.com/schemas/1970#uploader' type='application/atom+xml' href="https://gdata.youtube.com/feeds/api/users/${video.authorId}?v=2"/>
<link rel='related' type='application/atom+xml' href="https://gdata.youtube.com/feeds/api/videos/${video.id}?v=2"/>
<link rel='self' type='application/atom+xml' href='https://gdata.youtube.com/feeds/api/playlists/8E2186857EE27746/PLyl9mKRbpNIpJC5B8qpcgKX8v8NI62Jho?v=2'/>
<author>
<name>${video.author.name}</name>
<uri>https://gdata.youtube.com/feeds/api/users/${video.author.id}</uri>
<yt:userId>${video.author.id}</yt:userId>
<name>${video.authorName}</name>
<uri>https://gdata.youtube.com/feeds/api/users/${video.authorId}</uri>
<yt:userId>${video.authorId}</yt:userId>
</author>
<yt:accessControl action='comment' permission='allowed'/>
<yt:accessControl action='commentVote' permission='allowed'/>
@ -67,38 +67,38 @@ export default (
<yt:accessControl action='autoPlay' permission='allowed'/>
<yt:accessControl action='syndicate' permission='allowed'/>
<gd:comments>
<gd:feedLink rel='https://gdata.youtube.com/schemas/1970#comments' href="${config.BASE_URL}/api/videos/${video.video_id}/comments?v=2" countHint='5'/>
<gd:feedLink rel='https://gdata.youtube.com/schemas/1970#comments' href="${config.BASE_URL}/api/videos/${video.id}/comments?v=2" countHint='5'/>
</gd:comments>
<yt:location>Paris ,FR</yt:location>
<media:group>
<media:category label='Howto &amp; Style' scheme='https://gdata.youtube.com/schemas/1970/categories.cat'>Howto</media:category>
<media:content url="https://www.youtube.com/v/${video.video_id}?version=3&amp;f=playlists&amp;app=youtube_gdata" type='application/x-shockwave-flash' medium='video' isDefault='true' expression='full' duration='0' yt:format='5'/>
<media:content url="${config.BASE_URL}/getvideo/${video.video_id}" type='video/3gpp' type='video/3gpp' medium='video' expression='full' duration='0' yt:format='1'/>
<media:content url="${config.BASE_URL}/getvideo/${video.video_id}" type='video/3gpp' type='video/3gpp' medium='video' expression='full' duration='0' yt:format='6'/>
<media:credit role='uploader' scheme='urn:youtube' yt:display="${video.author.name}" yt:type='partner'>${video.author.id}</media:credit>
<media:content url="https://www.youtube.com/v/${video.id}?version=3&amp;f=playlists&amp;app=youtube_gdata" type='application/x-shockwave-flash' medium='video' isDefault='true' expression='full' duration='0' yt:format='5'/>
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type='video/3gpp' type='video/3gpp' medium='video' expression='full' duration='0' yt:format='1'/>
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type='video/3gpp' type='video/3gpp' medium='video' expression='full' duration='0' yt:format='6'/>
<media:credit role='uploader' scheme='urn:youtube' yt:display="${video.authorName}" yt:type='partner'>${video.authorId}</media:credit>
<media:description type='plain'>${video.description}</media:description>
<media:keywords/>
<media:license type='text/html' href='https://www.youtube.com/t/terms'>youtube</media:license>
<media:player url="https://www.youtube.com/watch?v=${video.video_id}&amp;feature=youtube_gdata_player"/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.video_id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='default'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.video_id}/mqdefault.jpg" height='180' width='320' yt:name='mqdefault'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.video_id}/mqdefault.jpg" height='360' width='480' yt:name='hqdefault'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.video_id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='start'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.video_id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='middle'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.video_id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='end'/>
<media:content url="${config.BASE_URL}/getvideo/${video.video_id}" type="video/mp4" medium="video" isDefault="true" expression="full" duration="0" yt:format="3" />
<media:content url="${config.BASE_URL}/getvideo/${video.video_id}" type="video/3gpp" medium="video" expression="full" duration="0" yt:format="2" />
<media:content url="${config.BASE_URL}/getvideo/${video.video_id}" type="video/mp4" medium="video" expression="full" duration="0" yt:format="8" />
<media:content url="${config.BASE_URL}/getvideo/${video.video_id}" type="video/3gpp" medium="video" expression="full" duration="0" yt:format="9" />
<media:player url="https://www.youtube.com/watch?v=${video.id}&amp;feature=youtube_gdata_player"/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='default'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/mqdefault.jpg" height='180' width='320' yt:name='mqdefault'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/mqdefault.jpg" height='360' width='480' yt:name='hqdefault'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='start'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='middle'/>
<media:thumbnail url="https://i.ytimg.com/vi/${video.id}/default.jpg" height='90' width='120' time='00:00:00.000' yt:name='end'/>
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type="video/mp4" medium="video" isDefault="true" expression="full" duration="0" yt:format="3" />
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type="video/3gpp" medium="video" expression="full" duration="0" yt:format="2" />
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type="video/mp4" medium="video" expression="full" duration="0" yt:format="8" />
<media:content url="${config.BASE_URL}/getvideo/${video.id}" type="video/3gpp" medium="video" expression="full" duration="0" yt:format="9" />
<media:title type='plain'>${video.title}</media:title>
<yt:duration seconds="${video.length_text?.text}"/>
<yt:uploaded>${new Date().toISOString()}</yt:uploaded>
<yt:uploaderId>${video.author.id}</yt:uploaderId>
<yt:videoid>${video.video_id}</yt:videoid>
<yt:duration seconds="${video.duration}"/>
<yt:uploaded>${video.created}</yt:uploaded>
<yt:uploaderId>${video.authorId}</yt:uploaderId>
<yt:videoid>${video.id}</yt:videoid>
</media:group>
<gd:rating average='0' max='0' min='0' numRaters='0' rel='https://schemas.google.com/g/2005#overall'/>
<yt:recorded>1970-08-22</yt:recorded>
<yt:statistics favoriteCount='0' viewCount="${video.view_count?.text}"/>
<yt:statistics favoriteCount='0' viewCount="${video.viewCount}"/>
<yt:rating numDislikes='100' numLikes='10000'/>
<yt:position>1</yt:position>
</entry>