FEAT: Added MOre Video Statuses
Build, Push and Deploy CMS / build-push-deploy (push) Failing after 2m2s

This commit is contained in:
FahdShalhoub
2026-08-17 00:13:22 +03:00
parent 4b719f712d
commit aa32bb6ed8
10 changed files with 216 additions and 146 deletions
+26 -21
View File
@@ -29,13 +29,13 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.categoriesResponse"
"$ref": "#/definitions/api.CategoriesResponse"
}
},
"500": {
"description": "Categories could not be read from the database",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
}
}
@@ -43,7 +43,7 @@ const docTemplate = `{
},
"/api/videos": {
"post": {
"description": "Step 2 of the upload flow. Takes the storage key returned by POST /api/videos/presign — after the file has been PUT to the presigned URL — submits a MediaConvert transcoding job, and persists the video with its category links. The returned record has status \"processing\"; note that nothing currently updates that status once transcoding finishes.",
"description": "Step 2 of the upload flow. Takes the storage key returned by POST /api/videos/presign — after the file has been PUT to the presigned URL — submits a MediaConvert transcoding job, and persists the video with its category links. The returned record has status \"processing\" — one of the states \"processing\", \"ready\" or \"failed\" — though nothing currently moves it on once transcoding finishes.",
"consumes": [
"application/json"
],
@@ -61,7 +61,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.completeRequest"
"$ref": "#/definitions/api.CompleteRequest"
}
}
],
@@ -69,25 +69,25 @@ const docTemplate = `{
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handlers.videoResponse"
"$ref": "#/definitions/api.VideoResponse"
}
},
"400": {
"description": "Request body was not valid JSON",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
},
"422": {
"description": "title was empty, or key was missing or not a key issued by this API",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
},
"500": {
"description": "Transcoding job could not be queued, or the video record could not be saved",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
}
}
@@ -113,7 +113,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.presignRequest"
"$ref": "#/definitions/api.PresignRequest"
}
}
],
@@ -121,25 +121,25 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.presignResponse"
"$ref": "#/definitions/api.PresignResponse"
}
},
"400": {
"description": "Request body was not valid JSON",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
},
"422": {
"description": "contentType was not 'video/mp4' or 'video/quicktime'",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
},
"500": {
"description": "Upload URL could not be issued",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
}
}
@@ -159,7 +159,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.healthResponse"
"$ref": "#/definitions/api.HealthResponse"
}
}
}
@@ -167,7 +167,7 @@ const docTemplate = `{
}
},
"definitions": {
"handlers.categoriesResponse": {
"api.CategoriesResponse": {
"type": "object",
"properties": {
"categories": {
@@ -182,7 +182,7 @@ const docTemplate = `{
}
}
},
"handlers.completeRequest": {
"api.CompleteRequest": {
"type": "object",
"properties": {
"categories": {
@@ -217,7 +217,7 @@ const docTemplate = `{
}
}
},
"handlers.healthResponse": {
"api.HealthResponse": {
"type": "object",
"properties": {
"status": {
@@ -226,7 +226,7 @@ const docTemplate = `{
}
}
},
"handlers.presignRequest": {
"api.PresignRequest": {
"type": "object",
"properties": {
"contentType": {
@@ -243,7 +243,7 @@ const docTemplate = `{
}
}
},
"handlers.presignResponse": {
"api.PresignResponse": {
"type": "object",
"properties": {
"key": {
@@ -256,7 +256,7 @@ const docTemplate = `{
}
}
},
"handlers.problemDetails": {
"api.ProblemDetails": {
"type": "object",
"properties": {
"detail": {
@@ -277,7 +277,7 @@ const docTemplate = `{
}
}
},
"handlers.videoResponse": {
"api.VideoResponse": {
"type": "object",
"properties": {
"categories": {
@@ -315,6 +315,11 @@ const docTemplate = `{
},
"status": {
"type": "string",
"enum": [
"processing",
"ready",
"failed"
],
"example": "processing"
},
"storageKey": {
+26 -21
View File
@@ -22,13 +22,13 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.categoriesResponse"
"$ref": "#/definitions/api.CategoriesResponse"
}
},
"500": {
"description": "Categories could not be read from the database",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
}
}
@@ -36,7 +36,7 @@
},
"/api/videos": {
"post": {
"description": "Step 2 of the upload flow. Takes the storage key returned by POST /api/videos/presign — after the file has been PUT to the presigned URL — submits a MediaConvert transcoding job, and persists the video with its category links. The returned record has status \"processing\"; note that nothing currently updates that status once transcoding finishes.",
"description": "Step 2 of the upload flow. Takes the storage key returned by POST /api/videos/presign — after the file has been PUT to the presigned URL — submits a MediaConvert transcoding job, and persists the video with its category links. The returned record has status \"processing\" — one of the states \"processing\", \"ready\" or \"failed\" — though nothing currently moves it on once transcoding finishes.",
"consumes": [
"application/json"
],
@@ -54,7 +54,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.completeRequest"
"$ref": "#/definitions/api.CompleteRequest"
}
}
],
@@ -62,25 +62,25 @@
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handlers.videoResponse"
"$ref": "#/definitions/api.VideoResponse"
}
},
"400": {
"description": "Request body was not valid JSON",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
},
"422": {
"description": "title was empty, or key was missing or not a key issued by this API",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
},
"500": {
"description": "Transcoding job could not be queued, or the video record could not be saved",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
}
}
@@ -106,7 +106,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.presignRequest"
"$ref": "#/definitions/api.PresignRequest"
}
}
],
@@ -114,25 +114,25 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.presignResponse"
"$ref": "#/definitions/api.PresignResponse"
}
},
"400": {
"description": "Request body was not valid JSON",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
},
"422": {
"description": "contentType was not 'video/mp4' or 'video/quicktime'",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
},
"500": {
"description": "Upload URL could not be issued",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
"$ref": "#/definitions/api.ProblemDetails"
}
}
}
@@ -152,7 +152,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.healthResponse"
"$ref": "#/definitions/api.HealthResponse"
}
}
}
@@ -160,7 +160,7 @@
}
},
"definitions": {
"handlers.categoriesResponse": {
"api.CategoriesResponse": {
"type": "object",
"properties": {
"categories": {
@@ -175,7 +175,7 @@
}
}
},
"handlers.completeRequest": {
"api.CompleteRequest": {
"type": "object",
"properties": {
"categories": {
@@ -210,7 +210,7 @@
}
}
},
"handlers.healthResponse": {
"api.HealthResponse": {
"type": "object",
"properties": {
"status": {
@@ -219,7 +219,7 @@
}
}
},
"handlers.presignRequest": {
"api.PresignRequest": {
"type": "object",
"properties": {
"contentType": {
@@ -236,7 +236,7 @@
}
}
},
"handlers.presignResponse": {
"api.PresignResponse": {
"type": "object",
"properties": {
"key": {
@@ -249,7 +249,7 @@
}
}
},
"handlers.problemDetails": {
"api.ProblemDetails": {
"type": "object",
"properties": {
"detail": {
@@ -270,7 +270,7 @@
}
}
},
"handlers.videoResponse": {
"api.VideoResponse": {
"type": "object",
"properties": {
"categories": {
@@ -308,6 +308,11 @@
},
"status": {
"type": "string",
"enum": [
"processing",
"ready",
"failed"
],
"example": "processing"
},
"storageKey": {
+27 -22
View File
@@ -1,6 +1,6 @@
basePath: /
definitions:
handlers.categoriesResponse:
api.CategoriesResponse:
properties:
categories:
example:
@@ -10,7 +10,7 @@ definitions:
type: string
type: array
type: object
handlers.completeRequest:
api.CompleteRequest:
properties:
categories:
example:
@@ -35,13 +35,13 @@ definitions:
example: Inside the Newsroom
type: string
type: object
handlers.healthResponse:
api.HealthResponse:
properties:
status:
example: ok
type: string
type: object
handlers.presignRequest:
api.PresignRequest:
properties:
contentType:
enum:
@@ -53,7 +53,7 @@ definitions:
example: interview-cut.mov
type: string
type: object
handlers.presignResponse:
api.PresignResponse:
properties:
key:
example: videos/a1b2c3d4e5f6.mov
@@ -62,7 +62,7 @@ definitions:
example: https://raw-uploads-bucket.s3.amazonaws.com/videos/a1b2....mov?X-Amz-Signature=...
type: string
type: object
handlers.problemDetails:
api.ProblemDetails:
properties:
detail:
example: The 'title' field was absent or contained only whitespace. Every
@@ -78,7 +78,7 @@ definitions:
example: about:blank
type: string
type: object
handlers.videoResponse:
api.VideoResponse:
properties:
categories:
example:
@@ -105,6 +105,10 @@ definitions:
example: 60
type: integer
status:
enum:
- processing
- ready
- failed
example: processing
type: string
storageKey:
@@ -138,11 +142,11 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.categoriesResponse'
$ref: '#/definitions/api.CategoriesResponse'
"500":
description: Categories could not be read from the database
schema:
$ref: '#/definitions/handlers.problemDetails'
$ref: '#/definitions/api.ProblemDetails'
summary: List video categories
tags:
- categories
@@ -153,8 +157,9 @@ paths:
description: Step 2 of the upload flow. Takes the storage key returned by POST
/api/videos/presign — after the file has been PUT to the presigned URL — submits
a MediaConvert transcoding job, and persists the video with its category links.
The returned record has status "processing"; note that nothing currently updates
that status once transcoding finishes.
The returned record has status "processing" — one of the states "processing",
"ready" or "failed" — though nothing currently moves it on once transcoding
finishes.
parameters:
- description: Video metadata plus the storage key from the presign step. title
and key are required; categories must be values from GET /api/categories.
@@ -162,28 +167,28 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/handlers.completeRequest'
$ref: '#/definitions/api.CompleteRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/handlers.videoResponse'
$ref: '#/definitions/api.VideoResponse'
"400":
description: Request body was not valid JSON
schema:
$ref: '#/definitions/handlers.problemDetails'
$ref: '#/definitions/api.ProblemDetails'
"422":
description: title was empty, or key was missing or not a key issued by
this API
schema:
$ref: '#/definitions/handlers.problemDetails'
$ref: '#/definitions/api.ProblemDetails'
"500":
description: Transcoding job could not be queued, or the video record could
not be saved
schema:
$ref: '#/definitions/handlers.problemDetails'
$ref: '#/definitions/api.ProblemDetails'
summary: Register an uploaded video
tags:
- videos
@@ -204,26 +209,26 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/handlers.presignRequest'
$ref: '#/definitions/api.PresignRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.presignResponse'
$ref: '#/definitions/api.PresignResponse'
"400":
description: Request body was not valid JSON
schema:
$ref: '#/definitions/handlers.problemDetails'
$ref: '#/definitions/api.ProblemDetails'
"422":
description: contentType was not 'video/mp4' or 'video/quicktime'
schema:
$ref: '#/definitions/handlers.problemDetails'
$ref: '#/definitions/api.ProblemDetails'
"500":
description: Upload URL could not be issued
schema:
$ref: '#/definitions/handlers.problemDetails'
$ref: '#/definitions/api.ProblemDetails'
summary: Create a presigned upload URL
tags:
- videos
@@ -237,7 +242,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.healthResponse'
$ref: '#/definitions/api.HealthResponse'
summary: Health check
tags:
- system
+23
View File
@@ -0,0 +1,23 @@
// Package api holds the request and response bodies of the HTTP API — the
// wire contract, kept apart from the handlers that serve it. The swaggo
// annotations on the handlers reference these types by name (api.VideoResponse
// and so on), so renaming one changes the generated spec.
package api
// HealthResponse is the body of GET /health.
type HealthResponse struct {
Status string `json:"status" example:"ok"`
}
// ProblemDetails is an error body in the RFC 9457 "Problem Details for HTTP
// APIs" format. Type stays "about:blank" — the value RFC 9457 defines for
// problems with no dedicated documentation URI. Title is a short summary that
// stays identical for every occurrence of the same problem, so clients can
// branch on it; Detail explains this particular occurrence and is the only
// member that varies with request data.
type ProblemDetails struct {
Type string `json:"type" example:"about:blank"`
Title string `json:"title" example:"Title is required."`
Status int `json:"status" example:"422"`
Detail string `json:"detail,omitempty" example:"The 'title' field was absent or contained only whitespace. Every video needs a non-empty title."`
}
+48
View File
@@ -0,0 +1,48 @@
package api
import "time"
// CategoriesResponse is the body of GET /api/categories.
type CategoriesResponse struct {
Categories []string `json:"categories" example:"documentary,news"`
}
// PresignRequest is the body of POST /api/videos/presign.
type PresignRequest struct {
FileName string `json:"fileName" example:"interview-cut.mov"`
ContentType string `json:"contentType" enums:"video/mp4,video/quicktime" example:"video/quicktime"`
}
// PresignResponse is the body returned by POST /api/videos/presign.
type PresignResponse struct {
UploadURL string `json:"uploadUrl" example:"https://raw-uploads-bucket.s3.amazonaws.com/videos/a1b2....mov?X-Amz-Signature=..."`
Key string `json:"key" example:"videos/a1b2c3d4e5f6.mov"`
}
// CompleteRequest is the body of POST /api/videos.
type CompleteRequest struct {
Title string `json:"title" example:"Inside the Newsroom"`
Description string `json:"description" example:"A behind-the-scenes look at the evening bulletin."`
Categories []string `json:"categories" example:"documentary,news"`
Tags string `json:"tags" example:"media, press, riyadh"`
FileName string `json:"fileName" example:"interview-cut.mov"`
Key string `json:"key" example:"videos/a1b2c3d4e5f6.mov"`
}
// VideoResponse is a persisted video record as returned by POST /api/videos.
// Status carries the values of services.VideoStatus; the enums tag has to be
// widened by hand whenever that type gains a state.
type VideoResponse struct {
ID string `json:"id" example:"0199f3a1-7c2e-7b21-9f0d-1a2b3c4d5e6f"`
Title string `json:"title" example:"Inside the Newsroom"`
Description string `json:"description" example:"A behind-the-scenes look at the evening bulletin."`
Categories []string `json:"categories" example:"documentary,news"`
Tags string `json:"tags" example:"media, press, riyadh"`
FileName string `json:"fileName" example:"interview-cut.mov"`
StorageKey string `json:"storageKey" example:"videos/a1b2c3d4e5f6.mov"`
MediaConvertJobID string `json:"mediaConvertJobId" example:"1755300000000-abcdef"`
Status string `json:"status" enums:"processing,ready,failed" example:"processing"`
SizeBytes int64 `json:"sizeBytes" example:"60"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
+4 -20
View File
@@ -4,35 +4,19 @@ import (
"encoding/json"
"log"
"net/http"
"thamanyah/cms/v2/internal/api"
)
type healthResponse struct {
Status string `json:"status" example:"ok"`
}
// Health is the liveness probe the ALB target group polls.
//
// @Summary Health check
// @Description Reports that the service is up and serving. Used as the ALB target group health check; it does not verify the database or S3 connections.
// @Tags system
// @Produce json
// @Success 200 {object} healthResponse
// @Success 200 {object} api.HealthResponse
// @Router /health [get]
func Health(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, healthResponse{Status: "ok"})
}
// problemDetails is an error body in the RFC 9457 "Problem Details for HTTP
// APIs" format. Type stays "about:blank" — the value RFC 9457 defines for
// problems with no dedicated documentation URI. Title is a short summary that
// stays identical for every occurrence of the same problem, so clients can
// branch on it; Detail explains this particular occurrence and is the only
// member that varies with request data.
type problemDetails struct {
Type string `json:"type" example:"about:blank"`
Title string `json:"title" example:"Title is required."`
Status int `json:"status" example:"422"`
Detail string `json:"detail,omitempty" example:"The 'title' field was absent or contained only whitespace. Every video needs a non-empty title."`
writeJSON(w, http.StatusOK, api.HealthResponse{Status: "ok"})
}
func writeJSON(w http.ResponseWriter, status int, body any) {
@@ -40,7 +24,7 @@ func writeJSON(w http.ResponseWriter, status int, body any) {
}
func writeProblem(w http.ResponseWriter, status int, title, detail string) {
writeJSONContent(w, "application/problem+json", status, problemDetails{
writeJSONContent(w, "application/problem+json", status, api.ProblemDetails{
Type: "about:blank",
Title: title,
Status: status,
+21 -58
View File
@@ -9,6 +9,7 @@ import (
"net/http"
"path/filepath"
"strings"
"thamanyah/cms/v2/internal/api"
"thamanyah/cms/v2/internal/services"
"time"
)
@@ -19,18 +20,14 @@ const (
maxJSONBodySize = 1 << 20 // 1 MiB
)
type categoriesResponse struct {
Categories []string `json:"categories" example:"documentary,news"`
}
// ListCategories returns the categories a video may be assigned to.
//
// @Summary List video categories
// @Description Returns the fixed lookup set of categories a video can belong to. Values from this list are the only ones accepted in the `categories` field of POST /api/videos.
// @Tags categories
// @Produce json
// @Success 200 {object} categoriesResponse
// @Failure 500 {object} problemDetails "Categories could not be read from the database"
// @Success 200 {object} api.CategoriesResponse
// @Failure 500 {object} api.ProblemDetails "Categories could not be read from the database"
// @Router /api/categories [get]
func ListCategories(w http.ResponseWriter, r *http.Request) {
categories, err := services.DB.ListCategories(r.Context())
@@ -44,7 +41,7 @@ func ListCategories(w http.ResponseWriter, r *http.Request) {
if categories == nil {
categories = []string{}
}
writeJSON(w, http.StatusOK, categoriesResponse{Categories: categories})
writeJSON(w, http.StatusOK, api.CategoriesResponse{Categories: categories})
}
// allowedUploadContentTypes is the set of media types accepted for upload,
@@ -57,16 +54,6 @@ var allowedUploadContentTypes = map[string]struct{}{
"video/quicktime": {}, // .mov
}
type presignRequest struct {
FileName string `json:"fileName" example:"interview-cut.mov"`
ContentType string `json:"contentType" enums:"video/mp4,video/quicktime" example:"video/quicktime"`
}
type presignResponse struct {
UploadURL string `json:"uploadUrl" example:"https://raw-uploads-bucket.s3.amazonaws.com/videos/a1b2....mov?X-Amz-Signature=..."`
Key string `json:"key" example:"videos/a1b2c3d4e5f6.mov"`
}
// PresignVideoUpload issues a presigned S3 PUT URL for a video upload.
//
// @Summary Create a presigned upload URL
@@ -74,16 +61,16 @@ type presignResponse struct {
// @Tags videos
// @Accept json
// @Produce json
// @Param request body presignRequest true "Name and media type of the file to be uploaded. contentType must be exactly 'video/mp4' or 'video/quicktime'."
// @Success 200 {object} presignResponse
// @Failure 400 {object} problemDetails "Request body was not valid JSON"
// @Failure 422 {object} problemDetails "contentType was not 'video/mp4' or 'video/quicktime'"
// @Failure 500 {object} problemDetails "Upload URL could not be issued"
// @Param request body api.PresignRequest true "Name and media type of the file to be uploaded. contentType must be exactly 'video/mp4' or 'video/quicktime'."
// @Success 200 {object} api.PresignResponse
// @Failure 400 {object} api.ProblemDetails "Request body was not valid JSON"
// @Failure 422 {object} api.ProblemDetails "contentType was not 'video/mp4' or 'video/quicktime'"
// @Failure 500 {object} api.ProblemDetails "Upload URL could not be issued"
// @Router /api/videos/presign [post]
func PresignVideoUpload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxJSONBodySize)
var req presignRequest
var req api.PresignRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeProblem(w, http.StatusBadRequest, "Invalid request.",
fmt.Sprintf("The request body could not be parsed as JSON (%s). Send an object with the string fields 'fileName' and 'contentType', and keep the body under 1 MiB.", err))
@@ -111,50 +98,26 @@ func PresignVideoUpload(w http.ResponseWriter, r *http.Request) {
return
}
writeJSON(w, http.StatusOK, presignResponse{UploadURL: presignedURL, Key: key})
}
type completeRequest struct {
Title string `json:"title" example:"Inside the Newsroom"`
Description string `json:"description" example:"A behind-the-scenes look at the evening bulletin."`
Categories []string `json:"categories" example:"documentary,news"`
Tags string `json:"tags" example:"media, press, riyadh"`
FileName string `json:"fileName" example:"interview-cut.mov"`
Key string `json:"key" example:"videos/a1b2c3d4e5f6.mov"`
}
type videoResponse struct {
ID string `json:"id" example:"0199f3a1-7c2e-7b21-9f0d-1a2b3c4d5e6f"`
Title string `json:"title" example:"Inside the Newsroom"`
Description string `json:"description" example:"A behind-the-scenes look at the evening bulletin."`
Categories []string `json:"categories" example:"documentary,news"`
Tags string `json:"tags" example:"media, press, riyadh"`
FileName string `json:"fileName" example:"interview-cut.mov"`
StorageKey string `json:"storageKey" example:"videos/a1b2c3d4e5f6.mov"`
MediaConvertJobID string `json:"mediaConvertJobId" example:"1755300000000-abcdef"`
Status string `json:"status" example:"processing"`
SizeBytes int64 `json:"sizeBytes" example:"60"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
writeJSON(w, http.StatusOK, api.PresignResponse{UploadURL: presignedURL, Key: key})
}
// CompleteVideoUpload registers an uploaded video and queues it for transcoding.
//
// @Summary Register an uploaded video
// @Description Step 2 of the upload flow. Takes the storage key returned by POST /api/videos/presign — after the file has been PUT to the presigned URL — submits a MediaConvert transcoding job, and persists the video with its category links. The returned record has status "processing"; note that nothing currently updates that status once transcoding finishes.
// @Description Step 2 of the upload flow. Takes the storage key returned by POST /api/videos/presign — after the file has been PUT to the presigned URL — submits a MediaConvert transcoding job, and persists the video with its category links. The returned record has status "processing" — one of the states "processing", "ready" or "failed" — though nothing currently moves it on once transcoding finishes.
// @Tags videos
// @Accept json
// @Produce json
// @Param request body completeRequest true "Video metadata plus the storage key from the presign step. title and key are required; categories must be values from GET /api/categories."
// @Success 201 {object} videoResponse
// @Failure 400 {object} problemDetails "Request body was not valid JSON"
// @Failure 422 {object} problemDetails "title was empty, or key was missing or not a key issued by this API"
// @Failure 500 {object} problemDetails "Transcoding job could not be queued, or the video record could not be saved"
// @Param request body api.CompleteRequest true "Video metadata plus the storage key from the presign step. title and key are required; categories must be values from GET /api/categories."
// @Success 201 {object} api.VideoResponse
// @Failure 400 {object} api.ProblemDetails "Request body was not valid JSON"
// @Failure 422 {object} api.ProblemDetails "title was empty, or key was missing or not a key issued by this API"
// @Failure 500 {object} api.ProblemDetails "Transcoding job could not be queued, or the video record could not be saved"
// @Router /api/videos [post]
func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxJSONBodySize)
var req completeRequest
var req api.CompleteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeProblem(w, http.StatusBadRequest, "Invalid request.",
fmt.Sprintf("The request body could not be parsed as JSON (%s). Send an object containing at least 'title' and 'key', and keep the body under 1 MiB.", err))
@@ -198,7 +161,7 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
FileName: strings.TrimSpace(req.FileName),
StorageKey: key,
MediaConvertJobID: jobID,
Status: "processing",
Status: services.VideoStatusProcessing,
SizeBytes: 60,
})
if err != nil {
@@ -208,7 +171,7 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
return
}
writeJSON(w, http.StatusCreated, videoResponse{
writeJSON(w, http.StatusCreated, api.CompleteResponse{
ID: video.ID,
Title: video.Title,
Description: video.Description,
@@ -217,7 +180,7 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
FileName: video.FileName,
StorageKey: video.StorageKey,
MediaConvertJobID: video.MediaConvertJobID,
Status: video.Status,
Status: video.Status.String(),
SizeBytes: video.SizeBytes,
CreatedAt: video.CreatedAt,
UpdatedAt: video.UpdatedAt,
+2 -2
View File
@@ -20,7 +20,7 @@ type Video struct {
FileName string
StorageKey string
MediaConvertJobID string
Status string
Status VideoStatus
SizeBytes int64
CreatedAt time.Time
UpdatedAt time.Time
@@ -56,7 +56,7 @@ func (svc DBConcrete) CreateVideo(ctx context.Context, v Video) (Video, error) {
INSERT INTO videos (title, description, tags, file_name, storage_key, mediaconvert_job_id, status, size_bytes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, created_at, updated_at
`, v.Title, v.Description, v.Tags, v.FileName, v.StorageKey, v.MediaConvertJobID, v.Status, v.SizeBytes)
`, v.Title, v.Description, v.Tags, v.FileName, v.StorageKey, v.MediaConvertJobID, string(v.Status), v.SizeBytes)
if err := row.Scan(&v.ID, &v.CreatedAt, &v.UpdatedAt); err != nil {
return Video{}, err
+23
View File
@@ -0,0 +1,23 @@
package services
// VideoStatus is the transcoding lifecycle state of a video, stored verbatim
// in videos.status. The set is closed on both sides: the database rejects
// anything outside it via the videos_status_check constraint. Adding a state
// means adding a constant here, widening that constraint in a new migration,
// and extending the `enums` annotation on api.VideoResponse.Status.
type VideoStatus string
const (
// VideoStatusProcessing is set when the MediaConvert job is queued. Every
// video starts here, and nothing moves it on yet — see Known gaps.
VideoStatusProcessing VideoStatus = "processing"
// VideoStatusReady means transcoding finished and the output is in the
// encoded bucket, reachable through CloudFront.
VideoStatusReady VideoStatus = "ready"
// VideoStatusFailed means the MediaConvert job ended in an error.
VideoStatusFailed VideoStatus = "failed"
)
func (s VideoStatus) String() string {
return string(s)
}