FEAT: Added Swagger Docs
Build, Push and Deploy CMS / build-push-deploy (push) Successful in 2m20s

This commit is contained in:
FahdShalhoub
2026-08-16 23:42:00 +03:00
parent cdc5315401
commit f26ea98b52
8 changed files with 1073 additions and 26 deletions
+352
View File
@@ -0,0 +1,352 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/api/categories": {
"get": {
"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.",
"produces": [
"application/json"
],
"tags": [
"categories"
],
"summary": "List video categories",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.categoriesResponse"
}
},
"500": {
"description": "Categories could not be read from the database",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
}
}
}
},
"/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.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"videos"
],
"summary": "Register an uploaded video",
"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.",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.completeRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handlers.videoResponse"
}
},
"400": {
"description": "Request body was not valid JSON",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
},
"422": {
"description": "title was empty, or key was missing or not a key issued by this API",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
},
"500": {
"description": "Transcoding job could not be queued, or the video record could not be saved",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
}
}
}
},
"/api/videos/presign": {
"post": {
"description": "Step 1 of the upload flow. Returns a short-lived presigned S3 URL that the client PUTs the video file to directly, plus the storage key identifying it. Once the PUT succeeds, pass that same key to POST /api/videos to register the video and start transcoding. The file itself never passes through this API.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"videos"
],
"summary": "Create a presigned upload URL",
"parameters": [
{
"description": "Name and media type of the file to be uploaded. contentType must be a video/* type.",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.presignRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.presignResponse"
}
},
"400": {
"description": "Request body was not valid JSON",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
},
"422": {
"description": "contentType was not a video/* media type",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
},
"500": {
"description": "Upload URL could not be issued",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
}
}
}
},
"/health": {
"get": {
"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.",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Health check",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.healthResponse"
}
}
}
}
}
},
"definitions": {
"handlers.categoriesResponse": {
"type": "object",
"properties": {
"categories": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"documentary",
"news"
]
}
}
},
"handlers.completeRequest": {
"type": "object",
"properties": {
"categories": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"documentary",
"news"
]
},
"description": {
"type": "string",
"example": "A behind-the-scenes look at the evening bulletin."
},
"fileName": {
"type": "string",
"example": "interview-cut.mov"
},
"key": {
"type": "string",
"example": "videos/a1b2c3d4e5f6.mov"
},
"tags": {
"type": "string",
"example": "media, press, riyadh"
},
"title": {
"type": "string",
"example": "Inside the Newsroom"
}
}
},
"handlers.healthResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "ok"
}
}
},
"handlers.presignRequest": {
"type": "object",
"properties": {
"contentType": {
"type": "string",
"example": "video/quicktime"
},
"fileName": {
"type": "string",
"example": "interview-cut.mov"
}
}
},
"handlers.presignResponse": {
"type": "object",
"properties": {
"key": {
"type": "string",
"example": "videos/a1b2c3d4e5f6.mov"
},
"uploadUrl": {
"type": "string",
"example": "https://raw-uploads-bucket.s3.amazonaws.com/videos/a1b2....mov?X-Amz-Signature=..."
}
}
},
"handlers.problemDetails": {
"type": "object",
"properties": {
"detail": {
"type": "string",
"example": "The 'title' field was absent or contained only whitespace. Every video needs a non-empty title."
},
"status": {
"type": "integer",
"example": 422
},
"title": {
"type": "string",
"example": "Title is required."
},
"type": {
"type": "string",
"example": "about:blank"
}
}
},
"handlers.videoResponse": {
"type": "object",
"properties": {
"categories": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"documentary",
"news"
]
},
"createdAt": {
"type": "string"
},
"description": {
"type": "string",
"example": "A behind-the-scenes look at the evening bulletin."
},
"fileName": {
"type": "string",
"example": "interview-cut.mov"
},
"id": {
"type": "string",
"example": "0199f3a1-7c2e-7b21-9f0d-1a2b3c4d5e6f"
},
"mediaConvertJobId": {
"type": "string",
"example": "1755300000000-abcdef"
},
"sizeBytes": {
"type": "integer",
"example": 60
},
"status": {
"type": "string",
"example": "processing"
},
"storageKey": {
"type": "string",
"example": "videos/a1b2c3d4e5f6.mov"
},
"tags": {
"type": "string",
"example": "media, press, riyadh"
},
"title": {
"type": "string",
"example": "Inside the Newsroom"
},
"updatedAt": {
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "",
BasePath: "/",
Schemes: []string{},
Title: "Thamanyah CMS API",
Description: "JSON API for ingesting videos into the Thamanyah catalogue. Uploads are two-step: presign, PUT the file straight to S3, then register the video to queue transcoding.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
+327
View File
@@ -0,0 +1,327 @@
{
"swagger": "2.0",
"info": {
"description": "JSON API for ingesting videos into the Thamanyah catalogue. Uploads are two-step: presign, PUT the file straight to S3, then register the video to queue transcoding.",
"title": "Thamanyah CMS API",
"contact": {},
"version": "1.0"
},
"basePath": "/",
"paths": {
"/api/categories": {
"get": {
"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.",
"produces": [
"application/json"
],
"tags": [
"categories"
],
"summary": "List video categories",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.categoriesResponse"
}
},
"500": {
"description": "Categories could not be read from the database",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
}
}
}
},
"/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.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"videos"
],
"summary": "Register an uploaded video",
"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.",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.completeRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handlers.videoResponse"
}
},
"400": {
"description": "Request body was not valid JSON",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
},
"422": {
"description": "title was empty, or key was missing or not a key issued by this API",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
},
"500": {
"description": "Transcoding job could not be queued, or the video record could not be saved",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
}
}
}
},
"/api/videos/presign": {
"post": {
"description": "Step 1 of the upload flow. Returns a short-lived presigned S3 URL that the client PUTs the video file to directly, plus the storage key identifying it. Once the PUT succeeds, pass that same key to POST /api/videos to register the video and start transcoding. The file itself never passes through this API.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"videos"
],
"summary": "Create a presigned upload URL",
"parameters": [
{
"description": "Name and media type of the file to be uploaded. contentType must be a video/* type.",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.presignRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.presignResponse"
}
},
"400": {
"description": "Request body was not valid JSON",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
},
"422": {
"description": "contentType was not a video/* media type",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
},
"500": {
"description": "Upload URL could not be issued",
"schema": {
"$ref": "#/definitions/handlers.problemDetails"
}
}
}
}
},
"/health": {
"get": {
"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.",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Health check",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.healthResponse"
}
}
}
}
}
},
"definitions": {
"handlers.categoriesResponse": {
"type": "object",
"properties": {
"categories": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"documentary",
"news"
]
}
}
},
"handlers.completeRequest": {
"type": "object",
"properties": {
"categories": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"documentary",
"news"
]
},
"description": {
"type": "string",
"example": "A behind-the-scenes look at the evening bulletin."
},
"fileName": {
"type": "string",
"example": "interview-cut.mov"
},
"key": {
"type": "string",
"example": "videos/a1b2c3d4e5f6.mov"
},
"tags": {
"type": "string",
"example": "media, press, riyadh"
},
"title": {
"type": "string",
"example": "Inside the Newsroom"
}
}
},
"handlers.healthResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "ok"
}
}
},
"handlers.presignRequest": {
"type": "object",
"properties": {
"contentType": {
"type": "string",
"example": "video/quicktime"
},
"fileName": {
"type": "string",
"example": "interview-cut.mov"
}
}
},
"handlers.presignResponse": {
"type": "object",
"properties": {
"key": {
"type": "string",
"example": "videos/a1b2c3d4e5f6.mov"
},
"uploadUrl": {
"type": "string",
"example": "https://raw-uploads-bucket.s3.amazonaws.com/videos/a1b2....mov?X-Amz-Signature=..."
}
}
},
"handlers.problemDetails": {
"type": "object",
"properties": {
"detail": {
"type": "string",
"example": "The 'title' field was absent or contained only whitespace. Every video needs a non-empty title."
},
"status": {
"type": "integer",
"example": 422
},
"title": {
"type": "string",
"example": "Title is required."
},
"type": {
"type": "string",
"example": "about:blank"
}
}
},
"handlers.videoResponse": {
"type": "object",
"properties": {
"categories": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"documentary",
"news"
]
},
"createdAt": {
"type": "string"
},
"description": {
"type": "string",
"example": "A behind-the-scenes look at the evening bulletin."
},
"fileName": {
"type": "string",
"example": "interview-cut.mov"
},
"id": {
"type": "string",
"example": "0199f3a1-7c2e-7b21-9f0d-1a2b3c4d5e6f"
},
"mediaConvertJobId": {
"type": "string",
"example": "1755300000000-abcdef"
},
"sizeBytes": {
"type": "integer",
"example": 60
},
"status": {
"type": "string",
"example": "processing"
},
"storageKey": {
"type": "string",
"example": "videos/a1b2c3d4e5f6.mov"
},
"tags": {
"type": "string",
"example": "media, press, riyadh"
},
"title": {
"type": "string",
"example": "Inside the Newsroom"
},
"updatedAt": {
"type": "string"
}
}
}
}
}
+240
View File
@@ -0,0 +1,240 @@
basePath: /
definitions:
handlers.categoriesResponse:
properties:
categories:
example:
- documentary
- news
items:
type: string
type: array
type: object
handlers.completeRequest:
properties:
categories:
example:
- documentary
- news
items:
type: string
type: array
description:
example: A behind-the-scenes look at the evening bulletin.
type: string
fileName:
example: interview-cut.mov
type: string
key:
example: videos/a1b2c3d4e5f6.mov
type: string
tags:
example: media, press, riyadh
type: string
title:
example: Inside the Newsroom
type: string
type: object
handlers.healthResponse:
properties:
status:
example: ok
type: string
type: object
handlers.presignRequest:
properties:
contentType:
example: video/quicktime
type: string
fileName:
example: interview-cut.mov
type: string
type: object
handlers.presignResponse:
properties:
key:
example: videos/a1b2c3d4e5f6.mov
type: string
uploadUrl:
example: https://raw-uploads-bucket.s3.amazonaws.com/videos/a1b2....mov?X-Amz-Signature=...
type: string
type: object
handlers.problemDetails:
properties:
detail:
example: The 'title' field was absent or contained only whitespace. Every
video needs a non-empty title.
type: string
status:
example: 422
type: integer
title:
example: Title is required.
type: string
type:
example: about:blank
type: string
type: object
handlers.videoResponse:
properties:
categories:
example:
- documentary
- news
items:
type: string
type: array
createdAt:
type: string
description:
example: A behind-the-scenes look at the evening bulletin.
type: string
fileName:
example: interview-cut.mov
type: string
id:
example: 0199f3a1-7c2e-7b21-9f0d-1a2b3c4d5e6f
type: string
mediaConvertJobId:
example: 1755300000000-abcdef
type: string
sizeBytes:
example: 60
type: integer
status:
example: processing
type: string
storageKey:
example: videos/a1b2c3d4e5f6.mov
type: string
tags:
example: media, press, riyadh
type: string
title:
example: Inside the Newsroom
type: string
updatedAt:
type: string
type: object
info:
contact: {}
description: 'JSON API for ingesting videos into the Thamanyah catalogue. Uploads
are two-step: presign, PUT the file straight to S3, then register the video to
queue transcoding.'
title: Thamanyah CMS API
version: "1.0"
paths:
/api/categories:
get:
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.
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.categoriesResponse'
"500":
description: Categories could not be read from the database
schema:
$ref: '#/definitions/handlers.problemDetails'
summary: List video categories
tags:
- categories
/api/videos:
post:
consumes:
- application/json
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.
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.
in: body
name: request
required: true
schema:
$ref: '#/definitions/handlers.completeRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/handlers.videoResponse'
"400":
description: Request body was not valid JSON
schema:
$ref: '#/definitions/handlers.problemDetails'
"422":
description: title was empty, or key was missing or not a key issued by
this API
schema:
$ref: '#/definitions/handlers.problemDetails'
"500":
description: Transcoding job could not be queued, or the video record could
not be saved
schema:
$ref: '#/definitions/handlers.problemDetails'
summary: Register an uploaded video
tags:
- videos
/api/videos/presign:
post:
consumes:
- application/json
description: Step 1 of the upload flow. Returns a short-lived presigned S3 URL
that the client PUTs the video file to directly, plus the storage key identifying
it. Once the PUT succeeds, pass that same key to POST /api/videos to register
the video and start transcoding. The file itself never passes through this
API.
parameters:
- description: Name and media type of the file to be uploaded. contentType must
be a video/* type.
in: body
name: request
required: true
schema:
$ref: '#/definitions/handlers.presignRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.presignResponse'
"400":
description: Request body was not valid JSON
schema:
$ref: '#/definitions/handlers.problemDetails'
"422":
description: contentType was not a video/* media type
schema:
$ref: '#/definitions/handlers.problemDetails'
"500":
description: Upload URL could not be issued
schema:
$ref: '#/definitions/handlers.problemDetails'
summary: Create a presigned upload URL
tags:
- videos
/health:
get:
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.
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.healthResponse'
summary: Health check
tags:
- system
swagger: "2.0"
+14
View File
@@ -9,9 +9,12 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.107.1
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/lib/pq v1.12.3
github.com/swaggo/http-swagger/v2 v2.0.2
github.com/swaggo/swag v1.16.6
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.17 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.35 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.36 // indirect
@@ -27,5 +30,16 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.45.5 // indirect
github.com/aws/smithy-go v1.27.7 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/tools v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+55
View File
@@ -1,5 +1,7 @@
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.43.5 h1:yKT5GYnFWhuDo+DqKvE5ZPwVn3RjC4MAeBtZGlh6AVM=
@@ -44,6 +46,9 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
@@ -62,28 +67,63 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
@@ -94,7 +134,22 @@ go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/Wgbsd
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+17 -5
View File
@@ -6,8 +6,20 @@ import (
"net/http"
)
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
// @Router /health [get]
func Health(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
writeJSON(w, http.StatusOK, healthResponse{Status: "ok"})
}
// problemDetails is an error body in the RFC 9457 "Problem Details for HTTP
@@ -17,10 +29,10 @@ func Health(w http.ResponseWriter, r *http.Request) {
// 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"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
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."`
}
func writeJSON(w http.ResponseWriter, status int, body any) {
+56 -21
View File
@@ -20,9 +20,18 @@ const (
)
type categoriesResponse struct {
Categories []string `json:"categories"`
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"
// @Router /api/categories [get]
func ListCategories(w http.ResponseWriter, r *http.Request) {
categories, err := services.DB.ListCategories(r.Context())
if err != nil {
@@ -39,15 +48,28 @@ func ListCategories(w http.ResponseWriter, r *http.Request) {
}
type presignRequest struct {
FileName string `json:"fileName"`
ContentType string `json:"contentType"`
FileName string `json:"fileName" example:"interview-cut.mov"`
ContentType string `json:"contentType" example:"video/quicktime"`
}
type presignResponse struct {
UploadURL string `json:"uploadUrl"`
Key string `json:"key"`
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
// @Description Step 1 of the upload flow. Returns a short-lived presigned S3 URL that the client PUTs the video file to directly, plus the storage key identifying it. Once the PUT succeeds, pass that same key to POST /api/videos to register the video and start transcoding. The file itself never passes through this API.
// @Tags videos
// @Accept json
// @Produce json
// @Param request body presignRequest true "Name and media type of the file to be uploaded. contentType must be a video/* type."
// @Success 200 {object} presignResponse
// @Failure 400 {object} problemDetails "Request body was not valid JSON"
// @Failure 422 {object} problemDetails "contentType was not a video/* media type"
// @Failure 500 {object} 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)
@@ -83,29 +105,42 @@ func PresignVideoUpload(w http.ResponseWriter, r *http.Request) {
}
type completeRequest struct {
Title string `json:"title"`
Description string `json:"description"`
Categories []string `json:"categories"`
Tags string `json:"tags"`
FileName string `json:"fileName"`
Key string `json:"key"`
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"`
Title string `json:"title"`
Description string `json:"description"`
Categories []string `json:"categories"`
Tags string `json:"tags"`
FileName string `json:"fileName"`
StorageKey string `json:"storageKey"`
MediaConvertJobID string `json:"mediaConvertJobId"`
Status string `json:"status"`
SizeBytes int64 `json:"sizeBytes"`
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"`
}
// 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.
// @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"
// @Router /api/videos [post]
func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxJSONBodySize)
+12
View File
@@ -13,8 +13,16 @@ import (
"github.com/aws/aws-sdk-go-v2/service/mediaconvert"
"github.com/aws/aws-sdk-go-v2/service/s3"
_ "github.com/lib/pq"
httpSwagger "github.com/swaggo/http-swagger/v2"
_ "thamanyah/cms/v2/docs"
)
// @title Thamanyah CMS API
// @version 1.0
// @description JSON API for ingesting videos into the Thamanyah catalogue. Uploads are two-step: presign, PUT the file straight to S3, then register the video to queue transcoding.
// @BasePath /
func main() {
if len(os.Args) > 1 && os.Args[1] == "migrate" {
runMigrate()
@@ -83,6 +91,10 @@ func runServer() {
mux.HandleFunc("POST /api/videos/presign", handlers.PresignVideoUpload)
mux.HandleFunc("POST /api/videos", handlers.CompleteVideoUpload)
// Swagger UI and the generated spec. The UI assets are embedded in the
// binary by swaggo/files, so this needs no static directory on disk.
mux.Handle("GET /swagger/", httpSwagger.WrapHandler)
addr := ":8081"
log.Printf("listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {