jbilcke-hf HF staff commited on
Commit
f70dd7e
1 Parent(s): d797bbb

added a view counter

Browse files
.env CHANGED
@@ -1,6 +1,8 @@
1
 
2
  NEXT_PUBLIC_SHOW_BETA_FEATURES="false"
3
 
 
 
4
  ADMIN_HUGGING_FACE_API_TOKEN=""
5
  ADMIN_HUGGING_FACE_USERNAME=""
6
 
@@ -9,6 +11,8 @@ AI_TUBE_ROBOT_API="https://jbilcke-hf-ai-tube-robot.hf.space"
9
  UPSTASH_REDIS_REST_URL=""
10
  UPSTASH_REDIS_REST_TOKEN=""
11
 
 
 
12
  # ----------- CENSORSHIP -------
13
  ENABLE_CENSORSHIP=
14
  FINGERPRINT_KEY=
 
1
 
2
  NEXT_PUBLIC_SHOW_BETA_FEATURES="false"
3
 
4
+ NEXT_PUBLIC_DEVELOPER_MODE="false"
5
+
6
  ADMIN_HUGGING_FACE_API_TOKEN=""
7
  ADMIN_HUGGING_FACE_USERNAME=""
8
 
 
11
  UPSTASH_REDIS_REST_URL=""
12
  UPSTASH_REDIS_REST_TOKEN=""
13
 
14
+ WINNERS=""
15
+ x
16
  # ----------- CENSORSHIP -------
17
  ENABLE_CENSORSHIP=
18
  FINGERPRINT_KEY=
src/app/config.ts CHANGED
@@ -4,4 +4,8 @@ export const showBetaFeatures = `${
4
 
5
 
6
  export const defaultVideoModel = "SVD"
7
- export const defaultVoice = "Julian"
 
 
 
 
 
4
 
5
 
6
  export const defaultVideoModel = "SVD"
7
+ export const defaultVoice = "Julian"
8
+
9
+ export const developerMode = `${
10
+ process.env.NEXT_PUBLIC_DEVELOPER_MODE || ""
11
+ }`.trim().toLowerCase() === "true"
src/app/interface/video-card/index.tsx CHANGED
@@ -213,7 +213,7 @@ export function VideoCard({
213
  isCompact ? `text-2xs lg:text-xs` : `text-sm`,
214
  `space-x-1`
215
  )}>
216
- <div>0 views</div>
217
  <div className="font-semibold scale-125">·</div>
218
  <div>{formatTimeAgo(video.updatedAt)}</div>
219
  </div>
 
213
  isCompact ? `text-2xs lg:text-xs` : `text-sm`,
214
  `space-x-1`
215
  )}>
216
+ <div>{video.numberOfViews} views</div>
217
  <div className="font-semibold scale-125">·</div>
218
  <div>{formatTimeAgo(video.updatedAt)}</div>
219
  </div>
src/app/server/actions/ai-tube-hf/extendVideosWithStats.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server"
2
+
3
+ import { VideoInfo } from "@/types"
4
+ import { getNumberOfViewsForVideos } from "../stats"
5
+
6
+ export async function extendVideosWithStats(videos: VideoInfo[]): Promise<VideoInfo[]> {
7
+
8
+ const stats = await getNumberOfViewsForVideos(videos.map(v => v.id))
9
+
10
+ return videos.map(v => {
11
+ v.numberOfViews = stats[v.id] || 0
12
+ return v
13
+ })
14
+ }
src/app/server/actions/ai-tube-hf/getChannelVideos.ts CHANGED
@@ -5,6 +5,7 @@ import { ChannelInfo, VideoInfo, VideoStatus } from "@/types"
5
  import { getVideoRequestsFromChannel } from "./getVideoRequestsFromChannel"
6
  import { adminApiKey } from "../config"
7
  import { getVideoIndex } from "./getVideoIndex"
 
8
 
9
  // return
10
  export async function getChannelVideos({
@@ -28,7 +29,7 @@ export async function getChannelVideos({
28
  const queued = await getVideoIndex({ status: "queued" })
29
  const published = await getVideoIndex({ status: "published" })
30
 
31
- return videos.map(v => {
32
  let video: VideoInfo = {
33
  id: v.id,
34
  status: "submitted",
@@ -57,10 +58,16 @@ export async function getChannelVideos({
57
 
58
  return video
59
  }).filter(video => {
 
60
  if (!status || typeof status === "undefined") {
61
  return true
62
  }
63
 
64
  return video.status === status
65
  })
 
 
 
 
 
66
  }
 
5
  import { getVideoRequestsFromChannel } from "./getVideoRequestsFromChannel"
6
  import { adminApiKey } from "../config"
7
  import { getVideoIndex } from "./getVideoIndex"
8
+ import { extendVideosWithStats } from "./extendVideosWithStats"
9
 
10
  // return
11
  export async function getChannelVideos({
 
29
  const queued = await getVideoIndex({ status: "queued" })
30
  const published = await getVideoIndex({ status: "published" })
31
 
32
+ const validVideos = videos.map(v => {
33
  let video: VideoInfo = {
34
  id: v.id,
35
  status: "submitted",
 
58
 
59
  return video
60
  }).filter(video => {
61
+ // if no filter is requested, we always return the video
62
  if (!status || typeof status === "undefined") {
63
  return true
64
  }
65
 
66
  return video.status === status
67
  })
68
+
69
+ // ask Redis for the freshest stats
70
+ const results = await extendVideosWithStats(validVideos)
71
+
72
+ return results
73
  }
src/app/server/actions/ai-tube-hf/getVideo.ts CHANGED
@@ -3,6 +3,7 @@
3
  import { VideoInfo } from "@/types"
4
 
5
  import { getVideoIndex } from "./getVideoIndex"
 
6
 
7
  export async function getVideo({
8
  videoId,
@@ -25,6 +26,8 @@ export async function getVideo({
25
  throw new Error(`cannot get the video, nothing found for id "${id}"`)
26
  }
27
 
 
 
28
  return video
29
  } catch (err) {
30
  if (neverThrow) {
 
3
  import { VideoInfo } from "@/types"
4
 
5
  import { getVideoIndex } from "./getVideoIndex"
6
+ import { getNumberOfViewsForVideo } from "../stats"
7
 
8
  export async function getVideo({
9
  videoId,
 
26
  throw new Error(`cannot get the video, nothing found for id "${id}"`)
27
  }
28
 
29
+ video.numberOfViews = await getNumberOfViewsForVideo(video.id)
30
+
31
  return video
32
  } catch (err) {
33
  if (neverThrow) {
src/app/server/actions/ai-tube-hf/getVideos.ts CHANGED
@@ -3,6 +3,7 @@
3
  import { VideoInfo } from "@/types"
4
 
5
  import { getVideoIndex } from "./getVideoIndex"
 
6
 
7
  const HARD_LIMIT = 100
8
 
@@ -90,5 +91,11 @@ export async function getVideos({
90
 
91
 
92
  // we enforce the max limit of HARD_LIMIT (eg. 100)
93
- return videosMatchingFilters.slice(0, Math.min(HARD_LIMIT, maxVideos))
 
 
 
 
 
 
94
  }
 
3
  import { VideoInfo } from "@/types"
4
 
5
  import { getVideoIndex } from "./getVideoIndex"
6
+ import { extendVideosWithStats } from "./extendVideosWithStats"
7
 
8
  const HARD_LIMIT = 100
9
 
 
91
 
92
 
93
  // we enforce the max limit of HARD_LIMIT (eg. 100)
94
+ const cappedVideos = videosMatchingFilters.slice(0, Math.min(HARD_LIMIT, maxVideos))
95
+
96
+
97
+ // finally, we ask Redis for the freshest stats
98
+ const videosWithStats = await extendVideosWithStats(cappedVideos)
99
+
100
+ return videosWithStats
101
  }
src/app/server/actions/config.ts CHANGED
@@ -7,3 +7,10 @@ export const adminUsername = `${process.env.ADMIN_HUGGING_FACE_USERNAME || ""}`
7
  export const adminCredentials: Credentials = { accessToken: adminApiKey }
8
 
9
  export const aiTubeRobotApi = `${process.env.AI_TUBE_ROBOT_API || ""}`
 
 
 
 
 
 
 
 
7
  export const adminCredentials: Credentials = { accessToken: adminApiKey }
8
 
9
  export const aiTubeRobotApi = `${process.env.AI_TUBE_ROBOT_API || ""}`
10
+
11
+ export const redisUrl = `${process.env.UPSTASH_REDIS_REST_URL || ""}`
12
+ export const redisToken = `${process.env.UPSTASH_REDIS_REST_TOKEN || ""}`
13
+
14
+ export const developerMode = `${
15
+ process.env.NEXT_PUBLIC_DEVELOPER_MODE || ""
16
+ }`.trim().toLowerCase() === "true"
src/app/server/actions/stats.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server"
2
+
3
+ import { Redis } from "@upstash/redis"
4
+
5
+ import { redisToken, redisUrl } from "./config"
6
+ import { developerMode } from "@/app/config"
7
+
8
+ const redis = new Redis({
9
+ url: redisUrl,
10
+ token: redisToken
11
+ })
12
+
13
+ export async function getNumberOfViewsForVideos(videoIds: string[]): Promise<Record<string, number>> {
14
+ if (!Array.isArray(videoIds)) {
15
+ return {}
16
+ }
17
+
18
+ try {
19
+
20
+ const stats: Record<string, number> = {}
21
+
22
+ const ids: string[] = []
23
+
24
+ for (const videoId of videoIds) {
25
+ ids.push(`videos:${videoId}:stats:views`)
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
+
37
+ return stats
38
+ } catch (err) {
39
+ return {}
40
+ }
41
+ }
42
+
43
+ export async function getNumberOfViewsForVideo(videoId: string): Promise<number> {
44
+ try {
45
+ const key = `videos:${videoId}:stats`
46
+
47
+ const result = await redis.get<number>(key) || 0
48
+
49
+ return result
50
+ } catch (err) {
51
+ return 0
52
+ }
53
+ }
54
+
55
+
56
+ export async function watchVideo(videoId: string): Promise<number> {
57
+ if (developerMode) {
58
+ return getNumberOfViewsForVideo(videoId)
59
+ }
60
+
61
+ try {
62
+ const result = await redis.incr(`videos:${videoId}:stats:views`)
63
+
64
+ return result
65
+ } catch (err) {
66
+ return 0
67
+ }
68
+ }
src/app/views/public-video-view/index.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client"
2
 
3
- import { useEffect, useState } from "react"
4
  import dynamic from "next/dynamic"
5
  import { RiCheckboxCircleFill } from "react-icons/ri"
6
  import { PiShareFatLight } from "react-icons/pi"
@@ -16,12 +16,15 @@ import { VideoInfo } from "@/types"
16
  import { ActionButton, actionButtonClassName } from "@/app/interface/action-button"
17
  import { RecommendedVideos } from "@/app/interface/recommended-videos"
18
  import { isCertifiedUser } from "@/app/certification"
 
 
19
 
20
  const DefaultAvatar = dynamic(() => import("../../interface/default-avatar"), {
21
  loading: () => null,
22
  })
23
 
24
  export function PublicVideoView() {
 
25
  const video = useStore(s => s.publicVideo)
26
 
27
  const videoId = `${video?.id || ""}`
@@ -67,6 +70,17 @@ export function PublicVideoView() {
67
  }
68
  }
69
 
 
 
 
 
 
 
 
 
 
 
 
70
  if (!video) { return null }
71
 
72
  return (
@@ -244,8 +258,12 @@ export function PublicVideoView() {
244
  `transition-all duration-200 ease-in-out`,
245
  `rounded-xl`,
246
  `bg-neutral-700/50`,
247
- `text-sm`,
248
  )}>
 
 
 
 
249
  <p>{video.description}</p>
250
  </div>
251
  </div>
 
1
  "use client"
2
 
3
+ import { useEffect, useState, useTransition } from "react"
4
  import dynamic from "next/dynamic"
5
  import { RiCheckboxCircleFill } from "react-icons/ri"
6
  import { PiShareFatLight } from "react-icons/pi"
 
16
  import { ActionButton, actionButtonClassName } from "@/app/interface/action-button"
17
  import { RecommendedVideos } from "@/app/interface/recommended-videos"
18
  import { isCertifiedUser } from "@/app/certification"
19
+ import { watchVideo } from "@/app/server/actions/stats"
20
+ import { formatTimeAgo } from "@/lib/formatTimeAgo"
21
 
22
  const DefaultAvatar = dynamic(() => import("../../interface/default-avatar"), {
23
  loading: () => null,
24
  })
25
 
26
  export function PublicVideoView() {
27
+ const [_pending, startTransition] = useTransition()
28
  const video = useStore(s => s.publicVideo)
29
 
30
  const videoId = `${video?.id || ""}`
 
70
  }
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
 
86
  return (
 
258
  `transition-all duration-200 ease-in-out`,
259
  `rounded-xl`,
260
  `bg-neutral-700/50`,
261
+ `text-sm text-zinc-100`,
262
  )}>
263
+ <div className="flex flex-row space-x-2 font-medium mb-1">
264
+ <div>{video.numberOfViews} views</div>
265
+ <div>{formatTimeAgo(video.updatedAt).replace("about ", "")}</div>
266
+ </div>
267
  <p>{video.description}</p>
268
  </div>
269
  </div>
src/lib/getChannelRating.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChannelInfo } from "@/types"
2
+
3
+ const winners = new Set(`${process.env.WINNERS || ""}`.toLowerCase().split(",").map(x => x.trim()).filter(x => x))
4
+
5
+ // TODO: replace by a better algorithm
6
+ export function getChannelRating(channel: ChannelInfo) {
7
+ if (winners.has(channel.datasetUser.toLowerCase())) { return 0 }
8
+
9
+ // TODO check views statistics to determine clusters
10
+
11
+ return 5
12
+ }