basic client that can fetch trending page
parent
098ee2d8ba
commit
b6c45a9049
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Trending
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://invidious.drgns.space/api/v1/trending
|
||||
body: none
|
||||
auth: none
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: Trending
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://pipedapi.kavin.rocks/trending?region=NL
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
region: NL
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "MaterialTube",
|
||||
"type": "collection"
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import Client from "@/client";
|
||||
import { ApiType } from "@/client/adapters";
|
||||
import { Component } from "@/typings/component";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const Video: Component = ({}) => {
|
||||
useEffect(() => {
|
||||
const client = new Client([
|
||||
{ baseUrl: "https://invidious.drgns.space", type: ApiType.Invidious },
|
||||
{ baseUrl: "https://pipedapi.kavin.rocks", type: ApiType.Piped }
|
||||
]);
|
||||
|
||||
client.getTrending("US").then(console.log);
|
||||
}, []);
|
||||
|
||||
return <></>;
|
||||
};
|
@ -1,9 +1,10 @@
|
||||
import { Button } from "@nextui-org/button";
|
||||
import { Video } from "./Video";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Button>Click me</Button>
|
||||
<Video />
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TrendingVideo } from "../typings/trending";
|
||||
|
||||
export interface ConnectedAdapter {
|
||||
getTrending(region: string): Promise<TrendingVideo[]>;
|
||||
}
|
||||
|
||||
export default interface Adapter {
|
||||
apiType: ApiType;
|
||||
|
||||
connect(url: string): ConnectedAdapter;
|
||||
}
|
||||
|
||||
export enum ApiType {
|
||||
Piped,
|
||||
Invidious
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import ky from "ky";
|
||||
|
||||
import Trending, { TrendingModel } from "./typings/trending";
|
||||
import Adapter, { ApiType } from "@/client/adapters";
|
||||
import Transformer from "./transformer";
|
||||
|
||||
const apiPath = (path: string): string => `/api/v1/${path}`;
|
||||
|
||||
export type TrendingVideoType = "music" | "gaming" | "news" | "movies";
|
||||
|
||||
export const getTrending = async (
|
||||
baseUrl: string,
|
||||
region?: string,
|
||||
type?: TrendingVideoType
|
||||
): Promise<Trending[]> => {
|
||||
const url = new URL(apiPath("trending"), baseUrl);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (region !== undefined) searchParams.append("region", region);
|
||||
|
||||
if (type !== undefined) searchParams.append("type", type);
|
||||
|
||||
const response = await ky.get(url, {
|
||||
searchParams
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
const data = TrendingModel.array().parse(json);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const adapter: Adapter = {
|
||||
apiType: ApiType.Invidious,
|
||||
|
||||
connect(url) {
|
||||
return {
|
||||
getTrending(region) {
|
||||
return getTrending(url, region).then(Transformer.trending);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default adapter;
|
@ -0,0 +1,23 @@
|
||||
import { TrendingVideo } from "@/client/typings/trending";
|
||||
import { Thumbnail } from "@/client/typings/thumbnail";
|
||||
|
||||
import InvidiousTrending from "./typings/trending";
|
||||
import InvidiousThumbnail from "./typings/thumbnail";
|
||||
|
||||
export default class Transformer {
|
||||
public static thumbnails(data: InvidiousThumbnail[]): Thumbnail[] {
|
||||
return data.map((thumbnail) => ({ url: thumbnail.url }));
|
||||
}
|
||||
|
||||
public static trending(data: InvidiousTrending[]): TrendingVideo[] {
|
||||
return data.map((video) => ({
|
||||
author: { id: video.authorId, name: video.author },
|
||||
duration: video.lengthSeconds * 1000,
|
||||
id: video.videoId,
|
||||
title: video.title,
|
||||
thumbnails: Transformer.thumbnails(video.videoThumbnails),
|
||||
uploaded: new Date(video.published * 1000 ?? 0),
|
||||
views: video.viewCount
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import z from "zod";
|
||||
|
||||
export const ThumbnailModel = z.object({
|
||||
quality: z.string(),
|
||||
url: z.string().url(),
|
||||
width: z.number(),
|
||||
height: z.number()
|
||||
});
|
||||
|
||||
type Thumbnail = z.infer<typeof ThumbnailModel>;
|
||||
|
||||
export default Thumbnail;
|
@ -0,0 +1,28 @@
|
||||
import z from "zod";
|
||||
import { ThumbnailModel } from "./thumbnail";
|
||||
|
||||
export const TrendingModel = z.object({
|
||||
title: z.string(),
|
||||
videoId: z.string(),
|
||||
videoThumbnails: ThumbnailModel.array(),
|
||||
|
||||
lengthSeconds: z.number(),
|
||||
viewCount: z.number(),
|
||||
|
||||
author: z.string(),
|
||||
authorId: z.string(),
|
||||
authorUrl: z.string(),
|
||||
|
||||
published: z.number(),
|
||||
publishedText: z.string(),
|
||||
description: z.string(),
|
||||
descriptionHtml: z.string(),
|
||||
|
||||
liveNow: z.boolean(),
|
||||
paid: z.boolean().optional().default(false),
|
||||
premium: z.boolean()
|
||||
});
|
||||
|
||||
type Trending = z.infer<typeof TrendingModel>;
|
||||
|
||||
export default Trending;
|
@ -0,0 +1,38 @@
|
||||
import ky from "ky";
|
||||
|
||||
import Adapter, { ApiType } from "@/client/adapters";
|
||||
|
||||
import Trending, { TrendingModel } from "./typings/trending";
|
||||
|
||||
import Transformer from "./transformer";
|
||||
|
||||
export const getTrending = async (
|
||||
apiBaseUrl: string,
|
||||
region = "US"
|
||||
): Promise<Trending[]> => {
|
||||
const url = new URL("/trending", apiBaseUrl);
|
||||
|
||||
const response = await ky.get(url, {
|
||||
searchParams: { region: region.toUpperCase() }
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
const data = TrendingModel.array().parse(json);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const adapter: Adapter = {
|
||||
apiType: ApiType.Piped,
|
||||
|
||||
connect(url) {
|
||||
return {
|
||||
getTrending(region) {
|
||||
return getTrending(url, region).then(Transformer.trending);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default adapter;
|
@ -0,0 +1,35 @@
|
||||
import { TrendingVideo } from "@/client/typings/trending";
|
||||
|
||||
import PipedTrending from "./typings/trending";
|
||||
|
||||
const videoIdRegex = /\/watch\?v=(.+)/;
|
||||
const channelIdRegex = /\/channel\/(.+)/;
|
||||
|
||||
export default class Transformer {
|
||||
public static trending(data: PipedTrending[]): TrendingVideo[] {
|
||||
return data.map((video) => {
|
||||
const videoIdMatch = video.url.match(videoIdRegex);
|
||||
|
||||
const videoId = videoIdMatch !== null ? videoIdMatch[1] : null;
|
||||
|
||||
if (videoId === null) throw new Error("Piped: Missing trending video id");
|
||||
|
||||
const channelIdMatch = video.uploaderUrl.match(channelIdRegex);
|
||||
|
||||
const channelId = channelIdMatch !== null ? channelIdMatch[1] : null;
|
||||
|
||||
if (channelId === null)
|
||||
throw new Error("Piped: Missing trending channelId");
|
||||
|
||||
return {
|
||||
duration: video.duration * 1000,
|
||||
views: video.views,
|
||||
id: videoId,
|
||||
uploaded: new Date(video.uploaded),
|
||||
thumbnails: [{ url: video.thumbnail }],
|
||||
title: video.title,
|
||||
author: { id: channelId, name: video.uploaderName }
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import z from "zod";
|
||||
|
||||
export const TrendingModel = z.object({
|
||||
duration: z.number(), // The duration of the trending video in seconds
|
||||
thumbnail: z.string().url(), // The thumbnail of the trending video
|
||||
title: z.string(), // The title of the trending video
|
||||
uploaded: z.number(),
|
||||
uploadedDate: z.string(), // The date the trending video was uploaded
|
||||
uploaderName: z.string(),
|
||||
uploaderAvatar: z.string().url(), // The avatar of the channel of the trending video
|
||||
uploaderUrl: z.string(), // The URL of the channel of the trending video
|
||||
uploaderVerified: z.boolean(), // Whether or not the channel of the trending video is verified
|
||||
url: z.string(), // The URL of the trending video
|
||||
views: z.number() // The number of views the trending video has
|
||||
});
|
||||
|
||||
type Trending = z.infer<typeof TrendingModel>;
|
||||
|
||||
export default Trending;
|
@ -0,0 +1,49 @@
|
||||
import { TrendingVideo } from "./typings/trending";
|
||||
|
||||
import InvidiousAdapter from "./adapters/invidious";
|
||||
import PipedAdapter from "./adapters/piped";
|
||||
|
||||
import Adapter, { ApiType } from "./adapters";
|
||||
|
||||
export interface RemoteApi {
|
||||
type: ApiType;
|
||||
baseUrl: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export default class Client {
|
||||
private apis: RemoteApi[];
|
||||
private adapters: Adapter[] = [InvidiousAdapter, PipedAdapter];
|
||||
|
||||
constructor(
|
||||
apis: {
|
||||
type: ApiType;
|
||||
baseUrl: string;
|
||||
}[]
|
||||
) {
|
||||
this.apis = apis.map((api) => ({ ...api, score: 0 }));
|
||||
}
|
||||
|
||||
private getAdapterForApiType(apiType: ApiType): Adapter {
|
||||
const adapter = this.adapters.find((adapter) => adapter.apiType == apiType);
|
||||
|
||||
if (adapter === undefined)
|
||||
throw new Error(`Could not find an adapter with api type ${apiType}`);
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private getBestApi(): RemoteApi {
|
||||
const randomIndex = Math.floor(Math.random() * this.apis.length);
|
||||
|
||||
return this.apis[randomIndex];
|
||||
}
|
||||
|
||||
public async getTrending(region: string): Promise<TrendingVideo[]> {
|
||||
const api = this.getBestApi();
|
||||
|
||||
const adapter = this.getAdapterForApiType(api.type);
|
||||
|
||||
return await adapter.connect(api.baseUrl).getTrending(region);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export interface Thumbnail {
|
||||
url: string;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { Thumbnail } from "./thumbnail";
|
||||
|
||||
export interface TrendingVideo {
|
||||
title: string;
|
||||
thumbnails: Thumbnail[];
|
||||
id: string;
|
||||
author: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
/*
|
||||
Duration in milliseconds.
|
||||
*/
|
||||
duration: number;
|
||||
views: number;
|
||||
uploaded: Date;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { FC, PropsWithChildren } from "react";
|
||||
|
||||
export type Component<P = unknown> = FC<PropsWithChildren<P>>;
|
Loading…
Reference in new issue