jbilcke-hf HF staff commited on
Commit
8f24b44
β€’
1 Parent(s): 3689a9d

preparing repo for multivideo

Browse files
next.config.js CHANGED
@@ -6,6 +6,7 @@ const nextConfig = {
6
  serverActions: true,
7
  },
8
 
 
9
  async redirects() {
10
  return [
11
  {
@@ -15,6 +16,7 @@ const nextConfig = {
15
  },
16
  ]
17
  },
 
18
  }
19
 
20
  module.exports = nextConfig
 
6
  serverActions: true,
7
  },
8
 
9
+ /*
10
  async redirects() {
11
  return [
12
  {
 
16
  },
17
  ]
18
  },
19
+ */
20
  }
21
 
22
  module.exports = nextConfig
scripts/test.js CHANGED
@@ -2,7 +2,7 @@ const { promises: fs } = require("node:fs")
2
 
3
  const main = async () => {
4
  console.log('generating shot..')
5
- const response = await fetch(process.env."http://localhost:3000/api/shot", {
6
  method: "POST",
7
  headers: {
8
  "Accept": "application/json",
 
2
 
3
  const main = async () => {
4
  console.log('generating shot..')
5
+ const response = await fetch("http://localhost:3000/api/shot", {
6
  method: "POST",
7
  headers: {
8
  "Accept": "application/json",
src/app/api/tasks/[uuid]/route.ts CHANGED
@@ -1,9 +1,9 @@
1
  import { NextRequest, NextResponse } from "next/server"
2
- import { getTask } from "@/server"
3
 
4
  // TODO: implement some kind of quota system
5
  export async function GET(req: NextRequest) {
6
  return NextResponse.json({
7
- task: await getTask(`${req.url?.split('/').pop() || ""}`)
8
  })
9
  }
 
1
  import { NextRequest, NextResponse } from "next/server"
2
+ import { getVideo } from "@/server"
3
 
4
  // TODO: implement some kind of quota system
5
  export async function GET(req: NextRequest) {
6
  return NextResponse.json({
7
+ video: await getVideo(`${req.url?.split('/').pop() || ""}`)
8
  })
9
  }
src/app/api/tasks/route.ts CHANGED
@@ -1,24 +1,23 @@
1
- import { VideoTask, VideoTaskRequest } from "@/app/types"
2
- import { getPendingTasks, submitNewTask } from "@/server"
3
  import { NextApiResponse } from "next"
4
  import { NextRequest, NextResponse } from "next/server"
5
 
6
  // TODO: implement some kind of quota system
7
  export async function GET() {
8
  return NextResponse.json({
9
- tasks: await getPendingTasks()
10
  })
11
  }
12
 
13
  // TODO: implement some kind of quota system
14
  export async function POST(
15
  req: NextRequest,
16
- res: NextApiResponse<VideoTask | {
17
  error?: string
18
  }>
19
  ) {
20
- console.log('POST req.body:', req.body)
21
- const taskRequest = req.body as VideoTaskRequest
22
- const task = await submitNewTask(taskRequest)
23
  res.status(200).json(task)
24
  }
 
1
+ import { Video, VideoRequest } from "@/app/types"
2
+ import { getAllVideos, createNewVideo } from "@/server"
3
  import { NextApiResponse } from "next"
4
  import { NextRequest, NextResponse } from "next/server"
5
 
6
  // TODO: implement some kind of quota system
7
  export async function GET() {
8
  return NextResponse.json({
9
+ videos: await getAllVideos()
10
  })
11
  }
12
 
13
  // TODO: implement some kind of quota system
14
  export async function POST(
15
  req: NextRequest,
16
+ res: NextApiResponse<Video | {
17
  error?: string
18
  }>
19
  ) {
20
+ const taskRequest = req.body as VideoRequest
21
+ const task = await createNewVideo(taskRequest)
 
22
  res.status(200).json(task)
23
  }
src/app/studio/[ownerId]/main.tsx CHANGED
@@ -2,20 +2,20 @@
2
 
3
  import { useState } from "react"
4
 
5
- import { VideoTasksQueue } from "@/components/business/tasks/video-tasks-queue"
6
  import { RefreshStudio } from "@/components/business/refresh"
7
  import { VideoForm } from "@/components/business/video-form"
8
- import { VideoTask } from "@/app/types"
9
  import { VideoPlayer } from "@/components/business/video-player"
10
 
11
- export default function Main({ videoTasks }: { videoTasks: VideoTask[] }) {
12
- const [selectedVideo, selectVideo] = useState<VideoTask>()
13
 
14
  return (
15
  <div className="flex flex-col md:flex-row">
16
  <div className="h-full flex flex-col space-y-4 w-full md:w-[800px] px-4 py-8">
17
  <VideoForm />
18
- <VideoTasksQueue videoTasks={videoTasks} onSelectVideo={selectVideo} />
19
  <RefreshStudio />
20
  </div>
21
  <div className="flex flex-col w-auto">
 
2
 
3
  import { useState } from "react"
4
 
5
+ import { VideosQueue } from "@/components/business/videos/video-table"
6
  import { RefreshStudio } from "@/components/business/refresh"
7
  import { VideoForm } from "@/components/business/video-form"
8
+ import { Video } from "@/app/types"
9
  import { VideoPlayer } from "@/components/business/video-player"
10
 
11
+ export default function Main({ videos }: { videos: Video[] }) {
12
+ const [selectedVideo, selectVideo] = useState<Video>()
13
 
14
  return (
15
  <div className="flex flex-col md:flex-row">
16
  <div className="h-full flex flex-col space-y-4 w-full md:w-[800px] px-4 py-8">
17
  <VideoForm />
18
+ <VideosQueue videos={videos} onSelectVideo={selectVideo} />
19
  <RefreshStudio />
20
  </div>
21
  <div className="flex flex-col w-auto">
src/app/studio/[ownerId]/page.tsx CHANGED
@@ -2,11 +2,12 @@
2
 
3
  import Head from "next/head"
4
 
 
 
5
  import Main from "./main"
6
- import { getTasks } from "@/server"
7
 
8
  export default async function StudioPage({ params: { ownerId } }: { params: { ownerId: string }}) {
9
- const videoTasks = await getTasks(ownerId)
10
 
11
  return (
12
  <div>
@@ -14,7 +15,7 @@ export default async function StudioPage({ params: { ownerId } }: { params: { ow
14
  <meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
15
  </Head>
16
  <main className="dark fixed inset-0 flex flex-col items-center bg-stone-900 text-stone-10 overflow-y-scroll">
17
- <Main videoTasks={videoTasks} />
18
  </main>
19
  </div>
20
  )
 
2
 
3
  import Head from "next/head"
4
 
5
+ import { getVideos } from "@/server"
6
+
7
  import Main from "./main"
 
8
 
9
  export default async function StudioPage({ params: { ownerId } }: { params: { ownerId: string }}) {
10
+ const videos = await getVideos(ownerId)
11
 
12
  return (
13
  <div>
 
15
  <meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
16
  </Head>
17
  <main className="dark fixed inset-0 flex flex-col items-center bg-stone-900 text-stone-10 overflow-y-scroll">
18
+ <Main videos={videos} />
19
  </main>
20
  </div>
21
  )
src/app/types.ts CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  export type VideoTransition =
2
  | 'dissolve'
3
  | 'bookflip'
@@ -139,6 +148,8 @@ export interface VideoShotMeta {
139
  export interface VideoShotData {
140
  // must be unique
141
  id: string
 
 
142
 
143
  fileName: string
144
 
@@ -158,6 +169,7 @@ export interface VideoShotData {
158
  nbCompletedSteps: number
159
  nbTotalSteps: number
160
  progressPercent: number
 
161
  completedAt: string
162
  completed: boolean
163
  error: string
@@ -204,15 +216,21 @@ export interface VideoSequenceMeta {
204
  export interface VideoSequenceData {
205
  // must be unique
206
  id: string
207
-
 
 
208
  fileName: string
209
 
210
  // used to check compatibility
211
  version: number
212
 
 
 
 
213
  hasAssembledVideo: boolean
214
  nbCompletedShots: number
215
  progressPercent: number
 
216
  completedAt: string
217
  completed: boolean
218
  error: string
@@ -220,13 +238,21 @@ export interface VideoSequenceData {
220
 
221
  export type VideoSequence = VideoSequenceMeta & VideoSequenceData
222
 
223
- export type VideoTaskRequest = Partial<{
 
 
 
 
 
 
 
 
 
224
  prompt: string
225
- ownerId: string
226
  sequence: Partial<VideoSequenceMeta>
227
  shots: Array<Partial<VideoShotMeta>>
228
  }>
229
 
230
- export type VideoTask = VideoSequence & {
231
  shots: VideoShot[]
232
  }
 
1
+ export type VideoStatus =
2
+ | 'pending'
3
+ | 'abort' // this is an order (the video might still being processed by a task)
4
+ | 'delete' // this is an order (the video might still being processed by a task)
5
+ | 'pause' // this is an order (the video might still being processed by a task)
6
+ | 'completed'
7
+ | 'unknown'
8
+
9
+
10
  export type VideoTransition =
11
  | 'dissolve'
12
  | 'bookflip'
 
148
  export interface VideoShotData {
149
  // must be unique
150
  id: string
151
+ sequenceId: string
152
+ ownerId: string
153
 
154
  fileName: string
155
 
 
169
  nbCompletedSteps: number
170
  nbTotalSteps: number
171
  progressPercent: number
172
+ createdAt: string
173
  completedAt: string
174
  completed: boolean
175
  error: string
 
216
  export interface VideoSequenceData {
217
  // must be unique
218
  id: string
219
+
220
+ ownerId: string
221
+
222
  fileName: string
223
 
224
  // used to check compatibility
225
  version: number
226
 
227
+ status: VideoStatus
228
+
229
+ hasGeneratedSpecs: boolean
230
  hasAssembledVideo: boolean
231
  nbCompletedShots: number
232
  progressPercent: number
233
+ createdAt: string
234
  completedAt: string
235
  completed: boolean
236
  error: string
 
238
 
239
  export type VideoSequence = VideoSequenceMeta & VideoSequenceData
240
 
241
+ export type VideoStatusRequest = {
242
+ status: VideoStatus
243
+ }
244
+
245
+ export type GenericAPIResponse = {
246
+ success?: boolean
247
+ error?: string
248
+ }
249
+
250
+ export type VideoAPIRequest = Partial<{
251
  prompt: string
 
252
  sequence: Partial<VideoSequenceMeta>
253
  shots: Array<Partial<VideoShotMeta>>
254
  }>
255
 
256
+ export type Video = VideoSequence & {
257
  shots: VideoShot[]
258
  }
src/components/business/video-form.tsx CHANGED
@@ -6,7 +6,7 @@ import { usePathname } from "next/navigation"
6
  import { experimental_useFormStatus as useFormStatus } from "react-dom"
7
  import { Textarea } from "@/components/ui/textarea"
8
  import { Button } from "@/components/ui/button"
9
- import { formSubmit } from "@/server/actions"
10
 
11
  export const VideoForm = () => {
12
  const pathname = usePathname()
@@ -15,7 +15,7 @@ export const VideoForm = () => {
15
 
16
  return (
17
  <form
18
- action={formSubmit}
19
  >
20
  <div className="flex flex-col md:hidden w-full text-center">
21
  <h2 className="text-4xl font-thin tracking-tight">VideoChain UI</h2>
 
6
  import { experimental_useFormStatus as useFormStatus } from "react-dom"
7
  import { Textarea } from "@/components/ui/textarea"
8
  import { Button } from "@/components/ui/button"
9
+ import { handleFormSubmit } from "@/server/actions"
10
 
11
  export const VideoForm = () => {
12
  const pathname = usePathname()
 
15
 
16
  return (
17
  <form
18
+ action={handleFormSubmit}
19
  >
20
  <div className="flex flex-col md:hidden w-full text-center">
21
  <h2 className="text-4xl font-thin tracking-tight">VideoChain UI</h2>
src/components/business/video-player.tsx CHANGED
@@ -1,8 +1,8 @@
1
  "use client"
2
 
3
- import { VideoTask } from "@/app/types"
4
 
5
- export const VideoPlayer = ({ video }: { video?: VideoTask }) => {
6
 
7
  if (typeof video === "undefined") {
8
  return <div className="flex w-full h-screen items-center justify-center text-center">
 
1
  "use client"
2
 
3
+ import { Video } from "@/app/types"
4
 
5
+ export const VideoPlayer = ({ video }: { video?: Video }) => {
6
 
7
  if (typeof video === "undefined") {
8
  return <div className="flex w-full h-screen items-center justify-center text-center">
src/components/business/{tasks/data-table-column-header.tsx β†’ videos/column-header.tsx} RENAMED
File without changes
src/components/business/{tasks β†’ videos}/columns.tsx RENAMED
@@ -3,12 +3,13 @@
3
  import { ColumnDef } from "@tanstack/react-table"
4
  import { Checkbox } from "@/components/ui/checkbox"
5
 
6
- import { DataTableColumnHeader } from "./data-table-column-header"
7
- import { DataTableRowActions } from "./data-table-row-actions"
8
 
9
- import { VideoTask } from "@/app/types"
 
10
 
11
- export const columns: ColumnDef<VideoTask>[] = [
12
  {
13
  id: "select",
14
  header: ({ table }) => (
@@ -42,15 +43,11 @@ export const columns: ColumnDef<VideoTask>[] = [
42
  header: ({ column }) => (
43
  <DataTableColumnHeader column={column} title="Prompt" />
44
  ),
45
- cell: ({ row }) => {
46
- return (
47
- <div className="flex space-x-2">
48
- <span className="max-w-[500px] font-medium">
49
- {row.getValue("videoPrompt")}
50
- </span>
51
- </div>
52
- )
53
- },
54
  enableSorting: false,
55
  },
56
  {
@@ -58,52 +55,50 @@ export const columns: ColumnDef<VideoTask>[] = [
58
  header: ({ column }) => (
59
  <DataTableColumnHeader column={column} title="Progress" />
60
  ),
61
- cell: ({ row }) => {
62
- const progress = Number(row.getValue("progressPercent") || 0)
63
-
64
- return (
65
- <div className="flex items-center">
66
- <span>{progress}%</span>
67
- </div>
68
- )
69
- },
70
  enableSorting: false,
71
  },
72
  {
73
- accessorKey: "preview",
74
- header: ({ column }) => (
75
- null // no header
76
- ),
77
- cell: ({ row }) => <div className="w-[100px]">
78
  <a
79
  className="hover:underline cursor-pointer"
80
  target="_blank"
81
- href={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${row.getValue("fileName")}`}>
82
- <video src={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${row.getValue("fileName")}?progress=${row.getValue("progressPercent") || 0}`} muted />
83
  </a>
84
  </div>,
85
  enableSorting: false,
86
  enableHiding: false,
87
  },
 
88
  {
89
  accessorKey: "fileName",
90
- header: ({ column }) => (
91
- null // no header
92
- ),
93
- cell: ({ row }) => <div className="">
94
  <a
95
  className="hover:underline cursor-pointer"
96
  target="_blank"
97
- href={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${row.getValue("fileName")}`}>Save</a>
98
  </div>,
99
  enableSorting: false,
100
  enableHiding: false,
101
  },
102
- /*
103
- action menu (currently disabled)
 
 
 
 
 
 
 
 
104
  {
105
  id: "actions",
106
- cell: ({ row }) => <DataTableRowActions row={row} />,
107
  },
108
- */
109
  ]
 
3
  import { ColumnDef } from "@tanstack/react-table"
4
  import { Checkbox } from "@/components/ui/checkbox"
5
 
6
+ import { DataTableColumnHeader } from "./column-header"
7
+ import { VideoActions } from "./video-actions"
8
 
9
+ import { Video } from "@/app/types"
10
+ import { deleteVideo } from "@/server"
11
 
12
+ export const columns: ColumnDef<Video>[] = [
13
  {
14
  id: "select",
15
  header: ({ table }) => (
 
43
  header: ({ column }) => (
44
  <DataTableColumnHeader column={column} title="Prompt" />
45
  ),
46
+ cell: ({ row: { original: { videoPrompt }} }) => (
47
+ <div className="flex space-x-2">
48
+ <span className="max-w-[500px] font-medium">{videoPrompt}</span>
49
+ </div>
50
+ ),
 
 
 
 
51
  enableSorting: false,
52
  },
53
  {
 
55
  header: ({ column }) => (
56
  <DataTableColumnHeader column={column} title="Progress" />
57
  ),
58
+ cell: ({ row: { original: { progressPercent }} }) => (
59
+ <div className="flex items-center"><span>{Number(progressPercent || 0)}%</span></div>
60
+ ),
 
 
 
 
 
 
61
  enableSorting: false,
62
  },
63
  {
64
+ accessorKey: "fileName",
65
+ header: ({ column }) => null,// no header
66
+ cell: ({ row: { original: { ownerId, id, progressPercent } } }) => <div className="w-[100px]">
 
 
67
  <a
68
  className="hover:underline cursor-pointer"
69
  target="_blank"
70
+ href={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${ownerId}/${id}.mp4`}>
71
+ <video src={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${ownerId}/${id}.mp4?progress=${progressPercent || 0}`} muted />
72
  </a>
73
  </div>,
74
  enableSorting: false,
75
  enableHiding: false,
76
  },
77
+ /*
78
  {
79
  accessorKey: "fileName",
80
+ header: ({ column }) => null,
81
+ cell: ({ row: { original: { fileName, ownerId, id }} }) => <div className="">
 
 
82
  <a
83
  className="hover:underline cursor-pointer"
84
  target="_blank"
85
+ href={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${ownerId}/${id}.mp4`}>Save</a>
86
  </div>,
87
  enableSorting: false,
88
  enableHiding: false,
89
  },
90
+ */
91
+ {
92
+ accessorKey: "delete",
93
+ header: ({ column }) => null, // no header
94
+ cell: ({ row: { original } }) => <div
95
+ className="hover:underline cursor-pointer"
96
+ onClick={() => { deleteVideo(original.ownerId, original.id) }}>Delete</div>,
97
+ enableSorting: false,
98
+ enableHiding: false,
99
+ },
100
  {
101
  id: "actions",
102
+ cell: ({ row }) => <VideoActions row={row} />,
103
  },
 
104
  ]
src/components/business/{tasks/data-table-row-actions.tsx β†’ videos/video-actions.tsx} RENAMED
@@ -13,12 +13,12 @@ import {
13
  DropdownMenuTrigger,
14
  } from "@/components/ui/dropdown-menu"
15
 
16
- import { VideoTask } from "@/app/types"
17
 
18
- export function DataTableRowActions({
19
  row,
20
  }: {
21
- row: Row<VideoTask>
22
  }) {
23
  const task = row.original
24
 
@@ -34,6 +34,10 @@ export function DataTableRowActions({
34
  </Button>
35
  </DropdownMenuTrigger>
36
  <DropdownMenuContent align="end" className="w-[160px]">
 
 
 
 
37
  <DropdownMenuItem>Download</DropdownMenuItem>
38
  <DropdownMenuSeparator />
39
  <DropdownMenuItem>
 
13
  DropdownMenuTrigger,
14
  } from "@/components/ui/dropdown-menu"
15
 
16
+ import { Video } from "@/app/types"
17
 
18
+ export function VideoActions({
19
  row,
20
  }: {
21
+ row: Row<Video>
22
  }) {
23
  const task = row.original
24
 
 
34
  </Button>
35
  </DropdownMenuTrigger>
36
  <DropdownMenuContent align="end" className="w-[160px]">
37
+ <DropdownMenuItem>
38
+ Pause generation
39
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
40
+ </DropdownMenuItem>
41
  <DropdownMenuItem>Download</DropdownMenuItem>
42
  <DropdownMenuSeparator />
43
  <DropdownMenuItem>
src/components/business/{tasks/video-tasks-queue.tsx β†’ videos/video-table.tsx} RENAMED
@@ -24,16 +24,16 @@ import {
24
  TableRow,
25
  } from "@/components/ui/table"
26
 
27
- import { columns } from "@/components/business/tasks/columns"
28
- import { VideoTask } from "@/app/types"
29
  import { useState } from "react"
30
 
31
- export function VideoTasksQueue({
32
- videoTasks = [],
33
  onSelectVideo,
34
  }: {
35
- videoTasks: VideoTask[]
36
- onSelectVideo: (task: VideoTask) => void
37
  }) {
38
  const [rowSelection, setRowSelection] = useState({})
39
  const [columnVisibility, setColumnVisibility] =
@@ -44,8 +44,8 @@ export function VideoTasksQueue({
44
  const [sorting, setSorting] = useState<SortingState>([])
45
 
46
  const table = useReactTable({
47
- data: videoTasks,
48
- columns: columns as ColumnDef<VideoTask, any>[],
49
  state: {
50
  sorting,
51
  columnVisibility,
@@ -98,7 +98,7 @@ export function VideoTasksQueue({
98
  className="cursor-pointer"
99
  onClick={() => {
100
  const videoId = `${row.getValue("id") || ""}`
101
- const video = videoTasks.find(({ id }) => id === videoId)
102
  if (video) {
103
  onSelectVideo(video)
104
  }
 
24
  TableRow,
25
  } from "@/components/ui/table"
26
 
27
+ import { columns } from "@/components/business/videos/columns"
28
+ import { Video } from "@/app/types"
29
  import { useState } from "react"
30
 
31
+ export function VideosQueue({
32
+ videos = [],
33
  onSelectVideo,
34
  }: {
35
+ videos: Video[]
36
+ onSelectVideo: (task: Video) => void
37
  }) {
38
  const [rowSelection, setRowSelection] = useState({})
39
  const [columnVisibility, setColumnVisibility] =
 
44
  const [sorting, setSorting] = useState<SortingState>([])
45
 
46
  const table = useReactTable({
47
+ data: videos,
48
+ columns: columns as ColumnDef<Video, any>[],
49
  state: {
50
  sorting,
51
  columnVisibility,
 
98
  className="cursor-pointer"
99
  onClick={() => {
100
  const videoId = `${row.getValue("id") || ""}`
101
+ const video = videos.find(({ id }) => id === videoId)
102
  if (video) {
103
  onSelectVideo(video)
104
  }
src/server/actions.ts CHANGED
@@ -1,17 +1,13 @@
1
  "use server"
2
 
3
  import { revalidatePath } from "next/cache"
4
- import { submitNewTask } from "."
5
-
6
- export async function formSubmit(formData: FormData) {
7
 
 
8
  const ownerId = `${formData.get("ownerId") || ""}`
9
- console.log('submitting to ', ownerId)
10
- await submitNewTask({
11
  prompt: `${formData.get("prompt") || ""}`,
12
- ownerId,
13
  })
14
- console.log('calling revalidate', ownerId)
15
  // for doc see https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
16
  revalidatePath(`/studio/${ownerId}`)
17
  }
 
1
  "use server"
2
 
3
  import { revalidatePath } from "next/cache"
4
+ import { createNewVideo } from "."
 
 
5
 
6
+ export async function handleFormSubmit(formData: FormData) {
7
  const ownerId = `${formData.get("ownerId") || ""}`
8
+ await createNewVideo(ownerId, {
 
9
  prompt: `${formData.get("prompt") || ""}`,
 
10
  })
 
11
  // for doc see https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
12
  revalidatePath(`/studio/${ownerId}`)
13
  }
src/server/base.ts CHANGED
@@ -3,7 +3,7 @@
3
  // so we have to add it ourselves if needed
4
  const apiUrl = process.env.VC_VIDEOCHAIN_API_URL
5
 
6
- export const get = async <T>(path: string = '', defaultValue: T): Promise<T> => {
7
  try {
8
  const res = await fetch(`${apiUrl}/${path}`, {
9
  method: "GET",
@@ -35,7 +35,38 @@ export const get = async <T>(path: string = '', defaultValue: T): Promise<T> =>
35
  }
36
 
37
 
38
- export const post = async <S, T>(path: string = '', payload: S, defaultValue: T): Promise<T> => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  try {
40
  const res = await fetch(`${apiUrl}/${path}`, {
41
  method: "POST",
@@ -60,6 +91,69 @@ export const post = async <S, T>(path: string = '', payload: S, defaultValue: T)
60
 
61
  const data = await res.json()
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  return ((data as T) || defaultValue)
64
  } catch (err) {
65
  return defaultValue
 
3
  // so we have to add it ourselves if needed
4
  const apiUrl = process.env.VC_VIDEOCHAIN_API_URL
5
 
6
+ export const GET = async <T>(path: string = '', defaultValue: T): Promise<T> => {
7
  try {
8
  const res = await fetch(`${apiUrl}/${path}`, {
9
  method: "GET",
 
35
  }
36
 
37
 
38
+ export const DELETE = async <T>(path: string = '', defaultValue: T): Promise<T> => {
39
+ try {
40
+ const res = await fetch(`${apiUrl}/${path}`, {
41
+ method: "DELETE",
42
+ headers: {
43
+ Accept: "application/json",
44
+ Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
45
+ },
46
+ cache: 'no-store',
47
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
48
+ // next: { revalidate: 1 }
49
+ })
50
+
51
+ // The return value is *not* serialized
52
+ // You can return Date, Map, Set, etc.
53
+
54
+ // Recommendation: handle errors
55
+ if (res.status !== 200) {
56
+ // This will activate the closest `error.js` Error Boundary
57
+ throw new Error('Failed to fetch data')
58
+ }
59
+
60
+ const data = await res.json()
61
+
62
+ return ((data as T) || defaultValue)
63
+ } catch (err) {
64
+ console.error(err)
65
+ return defaultValue
66
+ }
67
+ }
68
+
69
+ export const POST = async <S, T>(path: string = '', payload: S, defaultValue: T): Promise<T> => {
70
  try {
71
  const res = await fetch(`${apiUrl}/${path}`, {
72
  method: "POST",
 
91
 
92
  const data = await res.json()
93
 
94
+ return ((data as T) || defaultValue)
95
+ } catch (err) {
96
+ return defaultValue
97
+ }
98
+ }
99
+
100
+
101
+ export const PUT = async <S, T>(path: string = '', payload: S, defaultValue: T): Promise<T> => {
102
+ try {
103
+ const res = await fetch(`${apiUrl}/${path}`, {
104
+ method: "PUT",
105
+ headers: {
106
+ Accept: "application/json",
107
+ "Content-Type": "application/json",
108
+ Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
109
+ },
110
+ body: JSON.stringify(payload),
111
+ // cache: 'no-store',
112
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
113
+ next: { revalidate: 1 }
114
+ })
115
+ // The return value is *not* serialized
116
+ // You can return Date, Map, Set, etc.
117
+
118
+ // Recommendation: handle errors
119
+ if (res.status !== 200) {
120
+ // This will activate the closest `error.js` Error Boundary
121
+ throw new Error('Failed to post data')
122
+ }
123
+
124
+ const data = await res.json()
125
+
126
+ return ((data as T) || defaultValue)
127
+ } catch (err) {
128
+ return defaultValue
129
+ }
130
+ }
131
+
132
+ export const PATCH = async <S, T>(path: string = '', payload: S, defaultValue: T): Promise<T> => {
133
+ try {
134
+ const res = await fetch(`${apiUrl}/${path}`, {
135
+ method: "PATCH",
136
+ headers: {
137
+ Accept: "application/json",
138
+ "Content-Type": "application/json",
139
+ Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
140
+ },
141
+ body: JSON.stringify(payload),
142
+ // cache: 'no-store',
143
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
144
+ next: { revalidate: 1 }
145
+ })
146
+ // The return value is *not* serialized
147
+ // You can return Date, Map, Set, etc.
148
+
149
+ // Recommendation: handle errors
150
+ if (res.status !== 200) {
151
+ // This will activate the closest `error.js` Error Boundary
152
+ throw new Error('Failed to post data')
153
+ }
154
+
155
+ const data = await res.json()
156
+
157
  return ((data as T) || defaultValue)
158
  } catch (err) {
159
  return defaultValue
src/server/index.ts CHANGED
@@ -1,39 +1,60 @@
1
  "use server"
2
 
3
- import { VideoTask, VideoTaskRequest } from "@/app/types"
4
 
5
- import { get, post } from "./base"
6
 
7
  // note: for security purposes we do not directly expose the VideoChain API:
8
  // all calls are protected with a token, that way it the VideooChain API can stay
9
  // lightweight, security and quotas are handled outside
10
 
11
- // attention: this return *ALL* pending tasks, including those of other users
12
- export const getPendingTasks = async () => {
13
- const tasks = await get<VideoTask[]>("", [])
14
 
15
  return tasks
16
  }
17
 
18
  // return all tasks of a owner
19
- export const getTasks = async (ownerId: string) => {
20
- const tasks = await get<VideoTask[]>(`owner/${ownerId}`, [])
21
 
22
  return tasks
23
  }
24
 
25
- export const getTask = async (ownerAndVideoId: string) => {
26
- const task = await get<VideoTask>(ownerAndVideoId, null as unknown as VideoTask)
27
 
28
  return task
29
  }
30
 
31
- export const submitNewTask = async (taskRequest: VideoTaskRequest) => {
32
- const task = await post<VideoTaskRequest, VideoTask>(
33
- "",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  taskRequest,
35
- null as unknown as VideoTask
36
  )
37
 
38
  return task
39
- }
 
 
1
  "use server"
2
 
3
+ import { Video, VideoAPIRequest, GenericAPIResponse, VideoStatusRequest, VideoStatus } from "@/app/types"
4
 
5
+ import { GET, POST, DELETE, PATCH } from "./base"
6
 
7
  // note: for security purposes we do not directly expose the VideoChain API:
8
  // all calls are protected with a token, that way it the VideooChain API can stay
9
  // lightweight, security and quotas are handled outside
10
 
11
+ // this should be used by the admin only
12
+ export const getAllVideos = async () => {
13
+ const tasks = await GET<Video[]>("", [])
14
 
15
  return tasks
16
  }
17
 
18
  // return all tasks of a owner
19
+ export const getVideos = async (ownerId: string) => {
20
+ const tasks = await GET<Video[]>(ownerId, [])
21
 
22
  return tasks
23
  }
24
 
25
+ export const getVideo = async (ownerId: string, videoId: string) => {
26
+ const task = await GET<Video>(`${ownerId}/${videoId}`, null as unknown as Video)
27
 
28
  return task
29
  }
30
 
31
+ export const setVideoStatus = async (ownerId: string, videoId: string, status: VideoStatus) => {
32
+ const task = await PATCH<VideoStatusRequest, GenericAPIResponse>(`${ownerId}/${videoId}`, { status }, null as unknown as Video)
33
+
34
+ return task
35
+ }
36
+
37
+ export const deleteVideo = async (ownerId: string, videoId: string) => {
38
+ const task = await DELETE<GenericAPIResponse>(`${ownerId}/${videoId}`, { success: true })
39
+
40
+ return task
41
+ }
42
+
43
+ /*
44
+ export async function deleteVideos(ownerId: string, videoIds: string[]) {
45
+ const task = await DELETE<GenericAPIResponse>(ownerAndVideoId, { success: true })
46
+
47
+ return task
48
+ }
49
+ */
50
+
51
+ export const createNewVideo = async (ownerId: string, taskRequest: VideoAPIRequest) => {
52
+ const task = await POST<VideoAPIRequest, Video>(
53
+ ownerId,
54
  taskRequest,
55
+ null as unknown as Video
56
  )
57
 
58
  return task
59
+ }
60
+