diff --git a/src/app/(youtube)/channel/[id]/ChannelPage.tsx b/src/app/(youtube)/channel/[id]/ChannelPage.tsx new file mode 100644 index 0000000..74fb5ca --- /dev/null +++ b/src/app/(youtube)/channel/[id]/ChannelPage.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { FC } from "react"; + +import { useClient } from "@/hooks/useClient"; + +import { Container } from "@/components/Container"; + +export const ChannelPage: FC<{ channelId: string }> = ({ channelId }) => { + const client = useClient(); + + const { error } = useQuery({ + queryKey: ["channel", channelId], + queryFn: () => { + return client.getChannel(channelId); + } + }); + + console.log(error); + + return {channelId}; +}; diff --git a/src/app/(youtube)/channel/[id]/page.tsx b/src/app/(youtube)/channel/[id]/page.tsx new file mode 100644 index 0000000..756ff5e --- /dev/null +++ b/src/app/(youtube)/channel/[id]/page.tsx @@ -0,0 +1,16 @@ +import { NextPage } from "next"; +import { Suspense } from "react"; + +import { ChannelPage } from "./ChannelPage"; + +const Page: NextPage<{ params: { id: string } }> = ({ params }) => { + return ( + <> + + + + + ); +}; + +export default Page; diff --git a/src/client/adapters/index.ts b/src/client/adapters/index.ts index 1f1dae4..a414aeb 100644 --- a/src/client/adapters/index.ts +++ b/src/client/adapters/index.ts @@ -1,3 +1,4 @@ +import { Channel } from "@/client/typings/channel"; import { Comments } from "@/client/typings/comment"; import { SearchResults } from "@/client/typings/search"; import { SearchOptions } from "@/client/typings/search/options"; @@ -14,6 +15,8 @@ export interface ConnectedAdapter { getWatchable(videoId: string): Promise; getComments(videoId: string, repliesToken?: string): Promise; + + getChannel(channelId: string): Promise; } export default interface Adapter { diff --git a/src/client/adapters/piped/index.ts b/src/client/adapters/piped/index.ts index fecd8e3..8f71f7e 100644 --- a/src/client/adapters/piped/index.ts +++ b/src/client/adapters/piped/index.ts @@ -7,6 +7,7 @@ import Adapter, { ApiType } from "@/client/adapters"; import { Suggestions } from "@/client/typings/search/suggestions"; import Transformer from "./transformer"; +import Channel, { ChannelModel } from "./typings/channel"; import Comments, { CommentsModel } from "./typings/comments"; import Search, { SearchModel } from "./typings/search"; import Stream, { StreamModel } from "./typings/stream"; @@ -128,6 +129,28 @@ const getComments = async ( return data; }; +export const getChannel = async ( + apiBaseUrl: string, + channelId: string, + nextpage?: string +): Promise => { + const searchParams = new URLSearchParams(); + + let url; + if (nextpage) { + url = new URL(path.join("nextpage", "channel", channelId), apiBaseUrl); + searchParams.append("nextpage", nextpage); + } else url = new URL(path.join("channel", channelId), apiBaseUrl); + + const response = await ky.get(url, { searchParams }); + + const json = await response.json(); + + const data = ChannelModel.parse(json); + + return data; +}; + const adapter: Adapter = { apiType: ApiType.Piped, @@ -177,6 +200,10 @@ const adapter: Adapter = { return getComments(url, videoId, repliesToken).then( Transformer.comments ); + }, + + async getChannel(channelId) { + return getChannel(url, channelId, undefined).then(Transformer.channel); } }; } diff --git a/src/client/adapters/piped/transformer.ts b/src/client/adapters/piped/transformer.ts index f7466b2..8ab0253 100644 --- a/src/client/adapters/piped/transformer.ts +++ b/src/client/adapters/piped/transformer.ts @@ -1,3 +1,4 @@ +import { Channel } from "@/client/typings/channel"; import { Comments } from "@/client/typings/comment"; import { ChannelItem, @@ -15,6 +16,7 @@ import { } from "@/utils/parseIdFromUrl"; import { parseRelativeTime } from "@/utils/parseRelativeTime"; +import PipedChannel from "./typings/channel"; import PipedComments from "./typings/comments"; import PipedItem from "./typings/item"; import PipedSearch from "./typings/search"; @@ -88,7 +90,7 @@ export default class Transformer { author: { id: channelId, name: data.uploaderName, - avatar: data.uploaderAvatar + avatar: data.uploaderAvatar ?? undefined } }; } @@ -162,4 +164,16 @@ export default class Transformer { })) }; } + + public static channel(data: PipedChannel): Channel { + return { + name: data.name, + id: data.id, + description: data.description, + avatar: data.avatarUrl, + subscribers: data.subscriberCount, + banner: data.bannerUrl, + verified: data.verified + }; + } } diff --git a/src/client/adapters/piped/typings/channel.ts b/src/client/adapters/piped/typings/channel.ts new file mode 100644 index 0000000..0f26e6d --- /dev/null +++ b/src/client/adapters/piped/typings/channel.ts @@ -0,0 +1,29 @@ +import z from "zod"; + +import { ItemModel } from "./item"; + +export const tabEnum = [ + "shorts", + "albums", + "playlists", + "livestreams" +] as const; + +export const tabType = z.enum(tabEnum); + +export const ChannelModel = z.object({ + id: z.string(), + name: z.string(), + avatarUrl: z.string().url(), + bannerUrl: z.string().url(), + description: z.string(), + nextpage: z.string().nullable(), + subscriberCount: z.number(), + verified: z.boolean(), + relatedStreams: ItemModel.array(), + tabs: z.object({ name: tabType, data: z.string() }).array() +}); + +type Channel = z.infer; + +export default Channel; diff --git a/src/client/adapters/piped/typings/video.ts b/src/client/adapters/piped/typings/video.ts index 97553f0..2751c4a 100644 --- a/src/client/adapters/piped/typings/video.ts +++ b/src/client/adapters/piped/typings/video.ts @@ -7,7 +7,7 @@ export const VideoModel = z.object({ uploaded: z.number(), uploadedDate: z.string().nullable(), // The date the video was uploaded uploaderName: z.string(), - uploaderAvatar: z.string().url(), // The avatar of the channel of the video + uploaderAvatar: z.string().url().nullable(), // The avatar of the channel of the video uploaderUrl: z.string(), // The URL of the channel of the video uploaderVerified: z.boolean(), // Whether or not the channel of the video is verified url: z.string(), // The URL of the video diff --git a/src/client/index.ts b/src/client/index.ts index 5a17ffe..254b472 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,6 +1,7 @@ import Adapter, { ApiType, ConnectedAdapter } from "./adapters"; import InvidiousAdapter from "./adapters/invidious"; import PipedAdapter from "./adapters/piped"; +import { Channel } from "./typings/channel"; import { Comments } from "./typings/comment"; import { SearchResults } from "./typings/search"; import { SearchOptions } from "./typings/search/options"; @@ -91,4 +92,10 @@ export default class Client { return await adapter.getComments(videoId, repliesToken); } + + public async getChannel(channelId: string): Promise { + const adapter = this.getBestAdapter(); + + return await adapter.getChannel(channelId); + } } diff --git a/src/client/typings/channel.ts b/src/client/typings/channel.ts new file mode 100644 index 0000000..9601bac --- /dev/null +++ b/src/client/typings/channel.ts @@ -0,0 +1,9 @@ +import { Author } from "./author"; + +export interface Channel extends Author { + id: string; + subscribers: number; + description: string; + avatar: string; + banner?: string; +} diff --git a/src/hooks/useClient.ts b/src/hooks/useClient.ts index 78e63c7..16ea1a9 100644 --- a/src/hooks/useClient.ts +++ b/src/hooks/useClient.ts @@ -8,7 +8,7 @@ export const useClient = (): Client => { () => new Client([ // { baseUrl: "https://invidious.fdn.fr/", type: ApiType.Invidious } - { baseUrl: "https://pipedapi.kavin.rocks", type: ApiType.Piped } + { baseUrl: "https://pipedapi.drgns.space/", type: ApiType.Piped } ]) );