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 }
])
);