jbilcke-hf HF staff commited on
Commit
b161bd3
1 Parent(s): f70dd7e
src/app/channel/page.tsx CHANGED
@@ -1,8 +1,15 @@
1
  import { AppQueryProps } from "@/types"
2
  import { Main } from "../main"
3
  import { getChannel } from "../server/actions/ai-tube-hf/getChannel"
 
4
 
5
  export default async function ChannelPage({ searchParams: { c: channelId } }: AppQueryProps) {
6
  const channel = await getChannel({ channelId, neverThrow: true })
7
- return (<Main channel={channel} />)
 
 
 
 
 
 
8
  }
 
1
  import { AppQueryProps } from "@/types"
2
  import { Main } from "../main"
3
  import { getChannel } from "../server/actions/ai-tube-hf/getChannel"
4
+ import { getChannelVideos } from "../server/actions/ai-tube-hf/getChannelVideos"
5
 
6
  export default async function ChannelPage({ searchParams: { c: channelId } }: AppQueryProps) {
7
  const channel = await getChannel({ channelId, neverThrow: true })
8
+
9
+ const publicVideos = await getChannelVideos({
10
+ channel: channel,
11
+ status: "published",
12
+ })
13
+
14
+ return (<Main channel={channel} publicVideos={publicVideos} />)
15
  }
src/app/interface/channel-card/index.tsx CHANGED
@@ -7,6 +7,7 @@ import { IoAdd } from "react-icons/io5"
7
  import { cn } from "@/lib/utils"
8
  import { ChannelInfo } from "@/types"
9
  import { isCertifiedUser } from "@/app/certification"
 
10
 
11
  const DefaultAvatar = dynamic(() => import("../default-avatar"), {
12
  loading: () => null,
@@ -36,81 +37,83 @@ export function ChannelCard({
36
  const isCreateButton = !channel.id
37
 
38
  return (
39
- <div
40
- className={cn(
41
- `flex flex-col`,
42
- `items-center justify-center`,
43
- `space-y-1`,
44
- `w-52 h-52`,
45
- `rounded-lg`,
46
- `text-neutral-100/80`,
47
- isCreateButton ? '' : `hover:bg-neutral-800/30 hover:text-neutral-100/100`,
48
- `cursor-pointer`,
49
- className,
50
- )}
51
- onClick={() => {
52
- if (onClick) {
53
- onClick(channel)
54
- }
55
- }}
56
- >
57
  <div
58
  className={cn(
59
- `flex flex-col items-center justify-center`,
60
- `rounded-full overflow-hidden`,
61
- `w-26 h-26`
62
- )}
 
 
 
 
 
 
 
 
 
 
 
63
  >
64
- {isCreateButton
65
- ? <div className={cn(
66
- `flex flex-col justify-center items-center text-center`,
67
- `w-full h-full rounded-full`,
68
- `bg-neutral-700 hover:bg-neutral-600`,
69
- `border-2 border-neutral-400 hover:border-neutral-300`
70
- )}>
71
- <IoAdd className="w-8 h-8" />
72
- </div>
73
- : channelThumbnail
74
- ? <img
75
- src={channelThumbnail}
76
- onError={handleBadChannelThumbnail}
77
- />
78
- : <DefaultAvatar
79
- username={channel.datasetUser}
80
- bgColor="#fde047"
81
- textColor="#1c1917"
82
- width={104}
83
- roundShape
84
- />}
85
- </div>
 
 
 
 
 
 
 
86
 
87
- <div className={cn(
88
- `flex flex-col`,
89
- `items-center justify-center text-center`,
90
- `space-y-1`
91
- )}>
92
  <div className={cn(
93
- `text-center text-base font-medium text-zinc-100`,
94
- isCreateButton ? 'mt-2' : ''
95
- )}>{
96
- isCreateButton ? "Create a channel" : channel.label
97
- }</div>
98
- {/*<div className="text-center text-sm font-semibold">
99
- by <a href={
100
- `https://huggingface.co/${channel.datasetUser}`
101
- } target="_blank">@{channel.datasetUser}</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  </div>
103
- */}
104
- {!isCreateButton && <div className="flex flex-row items-center space-x-0.5">
105
- <div className="flex flex-row items-center text-center text-xs font-medium">@{channel.datasetUser}</div>
106
- {isCertifiedUser(channel.datasetUser) ? <div className="text-xs text-neutral-400"><RiCheckboxCircleFill className="" /></div> : null}
107
- </div>}
108
- {!isCreateButton && <div className="flex flex-row items-center justify-center text-neutral-400">
109
- <div className="text-center text-xs">{0} videos</div>
110
- <div className="px-1">-</div>
111
- <div className="text-center text-xs">{channel.likes} likes</div>
112
- </div>}
113
  </div>
114
- </div>
115
  )
116
  }
 
7
  import { cn } from "@/lib/utils"
8
  import { ChannelInfo } from "@/types"
9
  import { isCertifiedUser } from "@/app/certification"
10
+ import Link from "next/link"
11
 
12
  const DefaultAvatar = dynamic(() => import("../default-avatar"), {
13
  loading: () => null,
 
37
  const isCreateButton = !channel.id
38
 
39
  return (
40
+ // <Link href={`/channel?c=${channel.id}`}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  <div
42
  className={cn(
43
+ `flex flex-col`,
44
+ `items-center justify-center`,
45
+ `space-y-1`,
46
+ `w-52 h-52`,
47
+ `rounded-lg`,
48
+ `text-neutral-100/80`,
49
+ isCreateButton ? '' : `hover:bg-neutral-800/30 hover:text-neutral-100/100`,
50
+ `cursor-pointer`,
51
+ className,
52
+ )}
53
+ onClick={() => {
54
+ if (onClick) {
55
+ onClick(channel)
56
+ }
57
+ }}
58
  >
59
+ <div
60
+ className={cn(
61
+ `flex flex-col items-center justify-center`,
62
+ `rounded-full overflow-hidden`,
63
+ `w-26 h-26`
64
+ )}
65
+ >
66
+ {isCreateButton
67
+ ? <div className={cn(
68
+ `flex flex-col justify-center items-center text-center`,
69
+ `w-full h-full rounded-full`,
70
+ `bg-neutral-700 hover:bg-neutral-600`,
71
+ `border-2 border-neutral-400 hover:border-neutral-300`
72
+ )}>
73
+ <IoAdd className="w-8 h-8" />
74
+ </div>
75
+ : channelThumbnail
76
+ ? <img
77
+ src={channelThumbnail}
78
+ onError={handleBadChannelThumbnail}
79
+ />
80
+ : <DefaultAvatar
81
+ username={channel.datasetUser}
82
+ bgColor="#fde047"
83
+ textColor="#1c1917"
84
+ width={104}
85
+ roundShape
86
+ />}
87
+ </div>
88
 
 
 
 
 
 
89
  <div className={cn(
90
+ `flex flex-col`,
91
+ `items-center justify-center text-center`,
92
+ `space-y-1`
93
+ )}>
94
+ <div className={cn(
95
+ `text-center text-base font-medium text-zinc-100`,
96
+ isCreateButton ? 'mt-2' : ''
97
+ )}>{
98
+ isCreateButton ? "Create a channel" : channel.label
99
+ }</div>
100
+ {/*<div className="text-center text-sm font-semibold">
101
+ by <a href={
102
+ `https://huggingface.co/${channel.datasetUser}`
103
+ } target="_blank">@{channel.datasetUser}</a>
104
+ </div>
105
+ */}
106
+ {!isCreateButton && <div className="flex flex-row items-center space-x-0.5">
107
+ <div className="flex flex-row items-center text-center text-xs font-medium">@{channel.datasetUser}</div>
108
+ {isCertifiedUser(channel.datasetUser) ? <div className="text-xs text-neutral-400"><RiCheckboxCircleFill className="" /></div> : null}
109
+ </div>}
110
+ {!isCreateButton && <div className="flex flex-row items-center justify-center text-neutral-400">
111
+ <div className="text-center text-xs">{0} videos</div>
112
+ <div className="px-1">-</div>
113
+ <div className="text-center text-xs">{channel.likes} likes</div>
114
+ </div>}
115
  </div>
 
 
 
 
 
 
 
 
 
 
116
  </div>
117
+ // </Link>
118
  )
119
  }
src/app/interface/top-header/index.tsx CHANGED
@@ -36,7 +36,7 @@ export function TopHeader() {
36
 
37
 
38
  useEffect(() => {
39
- if (view === "public_video") {
40
  setHeaderMode("compact")
41
  setMenuMode("slider_hidden")
42
  } else {
 
36
 
37
 
38
  useEffect(() => {
39
+ if (view === "public_video" || view === "public_channel") {
40
  setHeaderMode("compact")
41
  setMenuMode("slider_hidden")
42
  } else {
src/app/main.tsx CHANGED
@@ -23,10 +23,12 @@ import { TubeLayout } from "./interface/tube-layout"
23
  // more easily
24
  export function Main({
25
  video,
 
26
  channel,
27
  }: {
28
  // server side params
29
  video?: VideoInfo
 
30
  channel?: ChannelInfo
31
  }) {
32
  const pathname = usePathname()
@@ -35,10 +37,21 @@ export function Main({
35
  const setPublicVideo = useStore(s => s.setPublicVideo)
36
  const setView = useStore(s => s.setView)
37
  const setPathname = useStore(s => s.setPathname)
 
 
38
 
39
  const videoId = `${video?.id || ""}`
40
  // console.log("Main video= "+ videoId)
41
 
 
 
 
 
 
 
 
 
 
42
  useEffect(() => {
43
  // note: it is important to ALWAYS set the current video to videoId
44
  // even if it's undefined
@@ -54,6 +67,24 @@ export function Main({
54
  }
55
  }, [videoId])
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  // this is critical: it sync the current route (coming from server-side)
59
  // with the zustand state manager
 
23
  // more easily
24
  export function Main({
25
  video,
26
+ publicVideos,
27
  channel,
28
  }: {
29
  // server side params
30
  video?: VideoInfo
31
+ publicVideos?: VideoInfo[]
32
  channel?: ChannelInfo
33
  }) {
34
  const pathname = usePathname()
 
37
  const setPublicVideo = useStore(s => s.setPublicVideo)
38
  const setView = useStore(s => s.setView)
39
  const setPathname = useStore(s => s.setPathname)
40
+ const setPublicChannel = useStore(s => s.setPublicChannel)
41
+ const setPublicVideos = useStore(s => s.setPublicVideos)
42
 
43
  const videoId = `${video?.id || ""}`
44
  // console.log("Main video= "+ videoId)
45
 
46
+ const publicVideoIds = (publicVideos || []).map(v => v.id || "").filter(x => x)
47
+
48
+ useEffect(() => {
49
+ if (!publicVideos?.length) { return }
50
+ // note: it is important to ALWAYS set the current video to videoId
51
+ // even if it's undefined
52
+ setPublicVideos(publicVideos)
53
+ }, publicVideoIds)
54
+
55
  useEffect(() => {
56
  // note: it is important to ALWAYS set the current video to videoId
57
  // even if it's undefined
 
67
  }
68
  }, [videoId])
69
 
70
+ const channelId = `${channel?.id || ""}`
71
+ // console.log("Main video= "+ videoId)
72
+
73
+ useEffect(() => {
74
+ // note: it is important to ALWAYS set the current video to videoId
75
+ // even if it's undefined
76
+ setPublicChannel(channel)
77
+
78
+ if (channelId) {
79
+ // this is a hack for hugging face:
80
+ // we allow the ?v=<id> param on the root of the domain
81
+ if (pathname !== "/channel") {
82
+ // console.log("we are on huggingface apparently!")
83
+ router.replace(`/channel?v=${channelId}`)
84
+ }
85
+ }
86
+ }, [channelId])
87
+
88
 
89
  // this is critical: it sync the current route (coming from server-side)
90
  // with the zustand state manager
src/app/server/actions/ai-tube-hf/getChannelVideos.ts CHANGED
@@ -12,12 +12,14 @@ export async function getChannelVideos({
12
  channel,
13
  status,
14
  }: {
15
- channel: ChannelInfo
16
 
17
  // filter videos by status
18
  status?: VideoStatus
19
  }): Promise<VideoInfo[]> {
20
 
 
 
21
  const videos = await getVideoRequestsFromChannel({
22
  channel,
23
  apiKey: adminApiKey,
 
12
  channel,
13
  status,
14
  }: {
15
+ channel?: ChannelInfo
16
 
17
  // filter videos by status
18
  status?: VideoStatus
19
  }): Promise<VideoInfo[]> {
20
 
21
+ if (!channel) { return [] }
22
+
23
  const videos = await getVideoRequestsFromChannel({
24
  channel,
25
  apiKey: adminApiKey,
src/app/server/actions/stats.ts CHANGED
@@ -26,11 +26,11 @@ export async function getNumberOfViewsForVideos(videoIds: string[]): Promise<Rec
26
  stats[videoId] = 0
27
  }
28
 
29
-
30
  const values = await redis.mget<number[]>(...ids)
31
 
32
  values.forEach((nbViews, i) => {
33
- const videoId = ids[i]
 
34
  stats[videoId] = nbViews || 0
35
  })
36
 
 
26
  stats[videoId] = 0
27
  }
28
 
 
29
  const values = await redis.mget<number[]>(...ids)
30
 
31
  values.forEach((nbViews, i) => {
32
+ const redisId = `${ids[i] || ""}`
33
+ const videoId = redisId.replace(":stats:views", "").replace("videos:", "")
34
  stats[videoId] = nbViews || 0
35
  })
36
 
src/app/views/home-view/index.tsx CHANGED
@@ -8,6 +8,7 @@ import { VideoInfo } from "@/types"
8
  import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
9
  import { VideoList } from "@/app/interface/video-list"
10
  import { getTags } from "@/app/server/actions/ai-tube-hf/getTags"
 
11
 
12
  export function HomeView() {
13
  const [_isPending, startTransition] = useTransition()
@@ -25,6 +26,9 @@ export function HomeView() {
25
  maxVideos: 25
26
  })
27
 
 
 
 
28
  setPublicVideos(videos)
29
  })
30
  }, [currentTag])
 
8
  import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
9
  import { VideoList } from "@/app/interface/video-list"
10
  import { getTags } from "@/app/server/actions/ai-tube-hf/getTags"
11
+ import { extendVideosWithStats } from "@/app/server/actions/ai-tube-hf/extendVideosWithStats"
12
 
13
  export function HomeView() {
14
  const [_isPending, startTransition] = useTransition()
 
26
  maxVideos: 25
27
  })
28
 
29
+ // due to some caching on the first function.. we update with fresh data!
30
+ // const updatedVideos = await extendVideosWithStats(videos)
31
+
32
  setPublicVideos(videos)
33
  })
34
  }, [currentTag])
src/app/views/public-channel-view/index.tsx CHANGED
@@ -1,12 +1,16 @@
1
  "use client"
2
 
3
- import { useEffect, useTransition } from "react"
 
4
 
5
  import { useStore } from "@/app/state/useStore"
6
  import { cn } from "@/lib/utils"
7
  import { VideoList } from "@/app/interface/video-list"
8
  import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos"
9
 
 
 
 
10
 
11
  export function PublicChannelView() {
12
  const [_isPending, startTransition] = useTransition()
@@ -14,7 +18,21 @@ export function PublicChannelView() {
14
  const publicVideos = useStore(s => s.publicVideos)
15
  const setPublicVideos = useStore(s => s.setPublicVideos)
16
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  useEffect(() => {
 
 
18
  if (!publicChannel) {
19
  return
20
  }
@@ -24,16 +42,69 @@ export function PublicChannelView() {
24
  channel: publicChannel,
25
  status: "published",
26
  })
 
27
  setPublicVideos(videos)
28
  })
29
 
30
  setPublicVideos([])
31
  }, [publicChannel, publicChannel?.id])
32
 
 
 
33
  return (
34
  <div className={cn(
35
  `flex flex-col`
36
  )}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  <VideoList
38
  videos={publicVideos}
39
  />
 
1
  "use client"
2
 
3
+ import { useEffect, useState, useTransition } from "react"
4
+ import dynamic from "next/dynamic"
5
 
6
  import { useStore } from "@/app/state/useStore"
7
  import { cn } from "@/lib/utils"
8
  import { VideoList } from "@/app/interface/video-list"
9
  import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos"
10
 
11
+ const DefaultAvatar = dynamic(() => import("../../interface/default-avatar"), {
12
+ loading: () => null,
13
+ })
14
 
15
  export function PublicChannelView() {
16
  const [_isPending, startTransition] = useTransition()
 
18
  const publicVideos = useStore(s => s.publicVideos)
19
  const setPublicVideos = useStore(s => s.setPublicVideos)
20
 
21
+ const [channelThumbnail, setChannelThumbnail] = useState(publicChannel?.thumbnail || "")
22
+
23
+ const handleBadChannelThumbnail = () => {
24
+ try {
25
+ if (channelThumbnail) {
26
+ setChannelThumbnail("")
27
+ }
28
+ } catch (err) {
29
+
30
+ }
31
+ }
32
+
33
  useEffect(() => {
34
+ setChannelThumbnail(publicChannel?.thumbnail || "")
35
+
36
  if (!publicChannel) {
37
  return
38
  }
 
42
  channel: publicChannel,
43
  status: "published",
44
  })
45
+ console.log("videos:", videos)
46
  setPublicVideos(videos)
47
  })
48
 
49
  setPublicVideos([])
50
  }, [publicChannel, publicChannel?.id])
51
 
52
+ if (!publicChannel) { return null }
53
+
54
  return (
55
  <div className={cn(
56
  `flex flex-col`
57
  )}>
58
+ {/* BANNER */}
59
+ <div className={cn(
60
+ `flex flex-col items-center justify-center w-full h-44`
61
+ )}>
62
+ {channelThumbnail
63
+ ? <img
64
+ src={channelThumbnail}
65
+ onError={handleBadChannelThumbnail}
66
+ className="w-full h-full overflow-hidden object-cover"
67
+ />
68
+ : <DefaultAvatar
69
+ username={publicChannel.datasetUser}
70
+ bgColor="#fde047"
71
+ textColor="#1c1917"
72
+ width={160}
73
+ roundShape
74
+ />}
75
+ </div>
76
+
77
+ {/* CHANNEL INFO - HORIZONTAL */}
78
+ <div className={cn(
79
+ `flex flex-row`
80
+ )}>
81
+
82
+ {/* AVATAR */}
83
+ <div className={cn(
84
+ `flex flex-col items-center justify-center w-full`
85
+ )}>
86
+ {channelThumbnail
87
+ ? <img
88
+ src={channelThumbnail}
89
+ onError={handleBadChannelThumbnail}
90
+ className="w-40 h-40 overflow-hidden"
91
+ />
92
+ : <DefaultAvatar
93
+ username={publicChannel.datasetUser}
94
+ bgColor="#fde047"
95
+ textColor="#1c1917"
96
+ width={160}
97
+ roundShape
98
+ />}
99
+ </div>
100
+
101
+ <div className={cn(
102
+ `flex flex-col items-center justify-center w-full`
103
+ )}>
104
+ <h3 className="tex-xl text-zinc-100">{publicChannel.label}</h3>
105
+ </div>
106
+ </div>
107
+
108
  <VideoList
109
  videos={publicVideos}
110
  />
src/app/views/public-video-view/index.tsx CHANGED
@@ -32,6 +32,7 @@ export function PublicVideoView() {
32
  const [copied, setCopied] = useState<boolean>(false)
33
 
34
  const [channelThumbnail, setChannelThumbnail] = useState(`${video?.channel.thumbnail || ""}`)
 
35
 
36
  // we inject the current videoId in the URL, if it's not already present
37
  // this is a hack for Hugging Face iframes
@@ -71,15 +72,19 @@ export function PublicVideoView() {
71
  }
72
 
73
  useEffect(() => {
74
- if (!videoId) {
75
- return
76
- }
77
-
78
  startTransition(async () => {
79
- await watchVideo(videoId)
 
 
 
 
 
 
 
 
80
  })
81
 
82
- }, [videoId])
83
 
84
  if (!video) { return null }
85
 
 
32
  const [copied, setCopied] = useState<boolean>(false)
33
 
34
  const [channelThumbnail, setChannelThumbnail] = useState(`${video?.channel.thumbnail || ""}`)
35
+ const setPublicVideo = useStore(s => s.setPublicVideo)
36
 
37
  // we inject the current videoId in the URL, if it's not already present
38
  // this is a hack for Hugging Face iframes
 
72
  }
73
 
74
  useEffect(() => {
 
 
 
 
75
  startTransition(async () => {
76
+ if (!video || !video.id) {
77
+ return
78
+ }
79
+ const numberOfViews = await watchVideo(videoId)
80
+
81
+ setPublicVideo({
82
+ ...video,
83
+ numberOfViews
84
+ })
85
  })
86
 
87
+ }, [video?.id])
88
 
89
  if (!video) { return null }
90