From 2aa2e6cd510bcec3f02568be15c22f65b2326052 Mon Sep 17 00:00:00 2001 From: senke Date: Fri, 24 Apr 2026 00:45:10 +0200 Subject: [PATCH] feat(openapi): annotate track CRUD handlers + regen spec (v1.0.8 B-annot) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First batch of the backend OpenAPI annotation campaign. Adds full swaggo annotations to the 8 handlers in internal/core/track/track_crud_handler.go so the resulting openapi.yaml exposes the track CRUD surface to orval-generated frontend clients. Handlers annotated (all under @Tags Track): - ListTracks — GET /tracks - GetTrack — GET /tracks/{id} - UpdateTrack — PUT /tracks/{id} (Auth, ownership) - GetLyrics — GET /tracks/{id}/lyrics - UpdateLyrics — PUT /tracks/{id}/lyrics (Auth, ownership) - DeleteTrack — DELETE /tracks/{id} (Auth, ownership) - BatchDeleteTracks — POST /tracks/batch/delete (Auth) - BatchUpdateTracks — POST /tracks/batch/update (Auth) Each block follows the established pattern (auth.go + marketplace.go): Summary / Description / Tags / Accept / Produce / Security when auth-required / Param (path/query/body) with concrete types / Success envelope typed via response.APIResponse{data=...} / Failure 400/401/403/404/500 / Router. make openapi: ✅ valid (Swagger 2.0) go build ./...: ✅ openapi.yaml: +490 LOC, 8 new paths exposed under /tracks. Part of the Option B campaign tracked in /home/senke/.claude/plans/audit-fonctionnel-wild-hickey.md. ~364 handlers total remain unannotated across 16 files in /internal/core/ and ~55 files in /internal/handlers/. Subsequent commits will annotate one handler file at a time so each regenerated spec stays bisectable. Co-Authored-By: Claude Opus 4.7 (1M context) --- veza-backend-api/docs/docs.go | 795 +++++++++++++++++- veza-backend-api/docs/swagger.json | 795 +++++++++++++++++- veza-backend-api/docs/swagger.yaml | 514 ++++++++++- .../internal/core/track/track_crud_handler.go | 102 +++ veza-backend-api/openapi.yaml | 520 +++++++++++- 5 files changed, 2692 insertions(+), 34 deletions(-) diff --git a/veza-backend-api/docs/docs.go b/veza-backend-api/docs/docs.go index e36c555d5..afb89c88c 100644 --- a/veza-backend-api/docs/docs.go +++ b/veza-backend-api/docs/docs.go @@ -2349,6 +2349,106 @@ const docTemplate = `{ } }, "/tracks": { + "get": { + "description": "List tracks with pagination, filters, sort. Cursor-based when ?cursor provided, otherwise page/limit offset.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "List tracks", + "parameters": [ + { + "type": "string", + "description": "Opaque pagination cursor (overrides page)", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number, 1-based (ignored if cursor set)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Items per page (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Filter by creator UUID", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by genre", + "name": "genre", + "in": "query" + }, + { + "type": "string", + "description": "Filter by audio format (mp3, flac, wav, ogg, m4a, aac)", + "name": "format", + "in": "query" + }, + { + "type": "string", + "default": "created_at", + "description": "Sort column (created_at, play_count, title)", + "name": "sort_by", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "pagination": { + "type": "object" + }, + "tracks": { + "type": "array", + "items": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid query params", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + }, "post": { "security": [ { @@ -2426,6 +2526,172 @@ const docTemplate = `{ } } }, + "/tracks/batch/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Soft-delete up to N tracks in one request. Per-track ownership checked (admin can delete others).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Batch delete tracks", + "parameters": [ + { + "description": "List of track UUIDs", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.BatchDeleteRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "deleted": { + "type": "array", + "items": { + "type": "string" + } + }, + "failed": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation / batch size exceeded", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, + "/tracks/batch/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Apply a partial metadata update to up to N tracks in one request. Per-track ownership checked.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Batch update tracks", + "parameters": [ + { + "description": "Track UUIDs + shared updates map", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.BatchUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "failed": { + "type": "array", + "items": { + "type": "object" + } + }, + "updated": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation / batch size exceeded", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, "/tracks/chunk": { "post": { "security": [ @@ -2812,6 +3078,240 @@ const docTemplate = `{ } } }, + "/tracks/{id}": { + "get": { + "description": "Retrieve a single track. Private play_count / like_count are omitted for non-owners (v0.10.3 F202).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track by ID", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "track": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update the metadata of an existing track. Caller must own the track or be admin.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Update track metadata", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated metadata", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.UpdateTrackRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "track": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation / invalid id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "403": { + "description": "Not owner / no admin", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Soft-delete a track (sets deleted_at). Caller must own the track or be admin.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Delete track", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "403": { + "description": "Not owner / no admin", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, "/tracks/{id}/comments": { "get": { "description": "Get paginated list of comments for a track", @@ -3066,6 +3566,161 @@ const docTemplate = `{ } } }, + "/tracks/{id}/lyrics": { + "get": { + "description": "Returns the current lyrics for a track, or null if no lyrics exist.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track lyrics", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "lyrics may be null", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "lyrics": { + "type": "object" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Replace the lyrics of a track. Caller must own the track or be admin.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Create or update track lyrics", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Lyrics payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.UpdateLyricsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "lyrics": { + "type": "object" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation / invalid id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "403": { + "description": "Not owner / no admin", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, "/tracks/{id}/status": { "get": { "security": [ @@ -4108,6 +4763,41 @@ const docTemplate = `{ } }, "definitions": { + "internal_core_track.BatchDeleteRequest": { + "type": "object", + "required": [ + "track_ids" + ], + "properties": { + "track_ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "internal_core_track.BatchUpdateRequest": { + "type": "object", + "required": [ + "track_ids", + "updates" + ], + "properties": { + "track_ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "updates": { + "type": "object", + "additionalProperties": true + } + } + }, "internal_core_track.CompleteChunkedUploadRequest": { "type": "object", "required": [ @@ -4140,6 +4830,68 @@ const docTemplate = `{ } } }, + "internal_core_track.UpdateLyricsRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + } + }, + "internal_core_track.UpdateTrackRequest": { + "type": "object", + "properties": { + "album": { + "type": "string", + "maxLength": 255 + }, + "artist": { + "type": "string", + "maxLength": 255 + }, + "bpm": { + "type": "integer", + "maximum": 300, + "minimum": 0 + }, + "genre": { + "description": "legacy, single", + "type": "string", + "maxLength": 100 + }, + "genres": { + "description": "v0.10.1: max 3, taxonomy slugs", + "type": "array", + "items": { + "type": "string" + } + }, + "is_public": { + "type": "boolean" + }, + "musical_key": { + "type": "string", + "maxLength": 10 + }, + "tags": { + "description": "v0.10.1: max 10, 30 chars each", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "year": { + "type": "integer", + "maximum": 2100, + "minimum": 1900 + } + } + }, "internal_handlers.APIResponse": { "type": "object", "properties": { @@ -4162,8 +4914,11 @@ const docTemplate = `{ "minLength": 1 }, "parent_id": { - "description": "Changed to *uuid.UUID", "type": "string" + }, + "timestamp": { + "description": "Position in seconds (0 = top-level, no specific time)", + "type": "number" } } }, @@ -4364,6 +5119,10 @@ const docTemplate = `{ "confirm_text": { "type": "string" }, + "keep_public_tracks": { + "description": "If true, public tracks remain (attributed to deleted account)", + "type": "boolean" + }, "password": { "type": "string" }, @@ -4777,6 +5536,10 @@ const docTemplate = `{ "promo_code_id": { "type": "string" }, + "refund_deadline": { + "description": "v0.12.0: 14-day refund deadline", + "type": "string" + }, "status": { "description": "pending, completed, failed, refunded", "type": "string" @@ -5120,6 +5883,14 @@ const docTemplate = `{ "id": { "type": "string" }, + "is_default_favorites": { + "description": "v0.10.4 F136", + "type": "boolean" + }, + "is_editorial": { + "description": "v0.10.4 F141", + "type": "boolean" + }, "is_public": { "type": "boolean" }, @@ -5261,15 +6032,9 @@ const docTemplate = `{ "is_public": { "type": "boolean" }, - "like_count": { - "type": "integer" - }, "musical_key": { "type": "string" }, - "play_count": { - "type": "integer" - }, "sample_rate": { "description": "Hz", "type": "integer" @@ -5386,6 +6151,14 @@ const docTemplate = `{ "description": "Virtual field for input", "type": "string" }, + "password_changed_at": { + "description": "F016: Password expiration tracking", + "type": "string" + }, + "promoted_to_creator_at": { + "description": "v1.0.6: set the first time a user self-promotes to ` + "`" + `role='creator'` + "`" + `\nvia POST /api/v1/users/me/upgrade-creator. NULL for users who never\ntook that path (still 'user', or promoted by an admin out-of-band).", + "type": "string" + }, "role": { "type": "string" }, @@ -5421,6 +6194,12 @@ const docTemplate = `{ } }, "securityDefinitions": { + "ApiKeyAuth": { + "description": "Developer API key (obtain from Developer Portal). Format: vza_xxxxx", + "type": "apiKey", + "name": "X-API-Key", + "in": "header" + }, "BearerAuth": { "type": "apiKey", "name": "Authorization", @@ -5432,7 +6211,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.2.0", - Host: "localhost:8080", + Host: "localhost:18080", BasePath: "/api/v1", Schemes: []string{}, Title: "Veza Backend API", diff --git a/veza-backend-api/docs/swagger.json b/veza-backend-api/docs/swagger.json index 1cf024c51..f1f3d85d0 100644 --- a/veza-backend-api/docs/swagger.json +++ b/veza-backend-api/docs/swagger.json @@ -15,7 +15,7 @@ }, "version": "1.2.0" }, - "host": "localhost:8080", + "host": "localhost:18080", "basePath": "/api/v1", "paths": { "/api/v1/dashboard": { @@ -2343,6 +2343,106 @@ } }, "/tracks": { + "get": { + "description": "List tracks with pagination, filters, sort. Cursor-based when ?cursor provided, otherwise page/limit offset.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "List tracks", + "parameters": [ + { + "type": "string", + "description": "Opaque pagination cursor (overrides page)", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number, 1-based (ignored if cursor set)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Items per page (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Filter by creator UUID", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by genre", + "name": "genre", + "in": "query" + }, + { + "type": "string", + "description": "Filter by audio format (mp3, flac, wav, ogg, m4a, aac)", + "name": "format", + "in": "query" + }, + { + "type": "string", + "default": "created_at", + "description": "Sort column (created_at, play_count, title)", + "name": "sort_by", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "pagination": { + "type": "object" + }, + "tracks": { + "type": "array", + "items": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid query params", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + }, "post": { "security": [ { @@ -2420,6 +2520,172 @@ } } }, + "/tracks/batch/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Soft-delete up to N tracks in one request. Per-track ownership checked (admin can delete others).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Batch delete tracks", + "parameters": [ + { + "description": "List of track UUIDs", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.BatchDeleteRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "deleted": { + "type": "array", + "items": { + "type": "string" + } + }, + "failed": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation / batch size exceeded", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, + "/tracks/batch/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Apply a partial metadata update to up to N tracks in one request. Per-track ownership checked.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Batch update tracks", + "parameters": [ + { + "description": "Track UUIDs + shared updates map", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.BatchUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "failed": { + "type": "array", + "items": { + "type": "object" + } + }, + "updated": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation / batch size exceeded", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, "/tracks/chunk": { "post": { "security": [ @@ -2806,6 +3072,240 @@ } } }, + "/tracks/{id}": { + "get": { + "description": "Retrieve a single track. Private play_count / like_count are omitted for non-owners (v0.10.3 F202).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track by ID", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "track": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update the metadata of an existing track. Caller must own the track or be admin.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Update track metadata", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated metadata", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.UpdateTrackRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "track": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation / invalid id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "403": { + "description": "Not owner / no admin", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Soft-delete a track (sets deleted_at). Caller must own the track or be admin.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Delete track", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "403": { + "description": "Not owner / no admin", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, "/tracks/{id}/comments": { "get": { "description": "Get paginated list of comments for a track", @@ -3060,6 +3560,161 @@ } } }, + "/tracks/{id}/lyrics": { + "get": { + "description": "Returns the current lyrics for a track, or null if no lyrics exist.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track lyrics", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "lyrics may be null", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "lyrics": { + "type": "object" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Replace the lyrics of a track. Caller must own the track or be admin.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Create or update track lyrics", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Lyrics payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.UpdateLyricsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "lyrics": { + "type": "object" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation / invalid id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "403": { + "description": "Not owner / no admin", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, "/tracks/{id}/status": { "get": { "security": [ @@ -4102,6 +4757,41 @@ } }, "definitions": { + "internal_core_track.BatchDeleteRequest": { + "type": "object", + "required": [ + "track_ids" + ], + "properties": { + "track_ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "internal_core_track.BatchUpdateRequest": { + "type": "object", + "required": [ + "track_ids", + "updates" + ], + "properties": { + "track_ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "updates": { + "type": "object", + "additionalProperties": true + } + } + }, "internal_core_track.CompleteChunkedUploadRequest": { "type": "object", "required": [ @@ -4134,6 +4824,68 @@ } } }, + "internal_core_track.UpdateLyricsRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + } + }, + "internal_core_track.UpdateTrackRequest": { + "type": "object", + "properties": { + "album": { + "type": "string", + "maxLength": 255 + }, + "artist": { + "type": "string", + "maxLength": 255 + }, + "bpm": { + "type": "integer", + "maximum": 300, + "minimum": 0 + }, + "genre": { + "description": "legacy, single", + "type": "string", + "maxLength": 100 + }, + "genres": { + "description": "v0.10.1: max 3, taxonomy slugs", + "type": "array", + "items": { + "type": "string" + } + }, + "is_public": { + "type": "boolean" + }, + "musical_key": { + "type": "string", + "maxLength": 10 + }, + "tags": { + "description": "v0.10.1: max 10, 30 chars each", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "year": { + "type": "integer", + "maximum": 2100, + "minimum": 1900 + } + } + }, "internal_handlers.APIResponse": { "type": "object", "properties": { @@ -4156,8 +4908,11 @@ "minLength": 1 }, "parent_id": { - "description": "Changed to *uuid.UUID", "type": "string" + }, + "timestamp": { + "description": "Position in seconds (0 = top-level, no specific time)", + "type": "number" } } }, @@ -4358,6 +5113,10 @@ "confirm_text": { "type": "string" }, + "keep_public_tracks": { + "description": "If true, public tracks remain (attributed to deleted account)", + "type": "boolean" + }, "password": { "type": "string" }, @@ -4771,6 +5530,10 @@ "promo_code_id": { "type": "string" }, + "refund_deadline": { + "description": "v0.12.0: 14-day refund deadline", + "type": "string" + }, "status": { "description": "pending, completed, failed, refunded", "type": "string" @@ -5114,6 +5877,14 @@ "id": { "type": "string" }, + "is_default_favorites": { + "description": "v0.10.4 F136", + "type": "boolean" + }, + "is_editorial": { + "description": "v0.10.4 F141", + "type": "boolean" + }, "is_public": { "type": "boolean" }, @@ -5255,15 +6026,9 @@ "is_public": { "type": "boolean" }, - "like_count": { - "type": "integer" - }, "musical_key": { "type": "string" }, - "play_count": { - "type": "integer" - }, "sample_rate": { "description": "Hz", "type": "integer" @@ -5380,6 +6145,14 @@ "description": "Virtual field for input", "type": "string" }, + "password_changed_at": { + "description": "F016: Password expiration tracking", + "type": "string" + }, + "promoted_to_creator_at": { + "description": "v1.0.6: set the first time a user self-promotes to `role='creator'`\nvia POST /api/v1/users/me/upgrade-creator. NULL for users who never\ntook that path (still 'user', or promoted by an admin out-of-band).", + "type": "string" + }, "role": { "type": "string" }, @@ -5415,6 +6188,12 @@ } }, "securityDefinitions": { + "ApiKeyAuth": { + "description": "Developer API key (obtain from Developer Portal). Format: vza_xxxxx", + "type": "apiKey", + "name": "X-API-Key", + "in": "header" + }, "BearerAuth": { "type": "apiKey", "name": "Authorization", diff --git a/veza-backend-api/docs/swagger.yaml b/veza-backend-api/docs/swagger.yaml index 1bb999e8d..37c39ee22 100644 --- a/veza-backend-api/docs/swagger.yaml +++ b/veza-backend-api/docs/swagger.yaml @@ -1,5 +1,29 @@ basePath: /api/v1 definitions: + internal_core_track.BatchDeleteRequest: + properties: + track_ids: + items: + type: string + minItems: 1 + type: array + required: + - track_ids + type: object + internal_core_track.BatchUpdateRequest: + properties: + track_ids: + items: + type: string + minItems: 1 + type: array + updates: + additionalProperties: true + type: object + required: + - track_ids + - updates + type: object internal_core_track.CompleteChunkedUploadRequest: properties: upload_id: @@ -22,6 +46,51 @@ definitions: - total_chunks - total_size type: object + internal_core_track.UpdateLyricsRequest: + properties: + content: + type: string + type: object + internal_core_track.UpdateTrackRequest: + properties: + album: + maxLength: 255 + type: string + artist: + maxLength: 255 + type: string + bpm: + maximum: 300 + minimum: 0 + type: integer + genre: + description: legacy, single + maxLength: 100 + type: string + genres: + description: 'v0.10.1: max 3, taxonomy slugs' + items: + type: string + type: array + is_public: + type: boolean + musical_key: + maxLength: 10 + type: string + tags: + description: 'v0.10.1: max 10, 30 chars each' + items: + type: string + type: array + title: + maxLength: 255 + minLength: 1 + type: string + year: + maximum: 2100 + minimum: 1900 + type: integer + type: object internal_handlers.APIResponse: properties: data: {} @@ -36,8 +105,10 @@ definitions: minLength: 1 type: string parent_id: - description: Changed to *uuid.UUID type: string + timestamp: + description: Position in seconds (0 = top-level, no specific time) + type: number required: - content type: object @@ -178,6 +249,9 @@ definitions: properties: confirm_text: type: string + keep_public_tracks: + description: If true, public tracks remain (attributed to deleted account) + type: boolean password: type: string reason: @@ -468,6 +542,9 @@ definitions: type: string promo_code_id: type: string + refund_deadline: + description: 'v0.12.0: 14-day refund deadline' + type: string status: description: pending, completed, failed, refunded type: string @@ -698,6 +775,12 @@ definitions: type: integer id: type: string + is_default_favorites: + description: v0.10.4 F136 + type: boolean + is_editorial: + description: v0.10.4 F141 + type: boolean is_public: type: boolean title: @@ -794,12 +877,8 @@ definitions: type: string is_public: type: boolean - like_count: - type: integer musical_key: type: string - play_count: - type: integer sample_rate: description: Hz type: integer @@ -880,6 +959,15 @@ definitions: password: description: Virtual field for input type: string + password_changed_at: + description: 'F016: Password expiration tracking' + type: string + promoted_to_creator_at: + description: |- + v1.0.6: set the first time a user self-promotes to `role='creator'` + via POST /api/v1/users/me/upgrade-creator. NULL for users who never + took that path (still 'user', or promoted by an admin out-of-band). + type: string role: type: string slug: @@ -902,7 +990,7 @@ definitions: success: type: boolean type: object -host: localhost:8080 +host: localhost:18080 info: contact: email: support@veza.app @@ -2350,6 +2438,71 @@ paths: tags: - Playlist /tracks: + get: + description: List tracks with pagination, filters, sort. Cursor-based when ?cursor + provided, otherwise page/limit offset. + parameters: + - description: Opaque pagination cursor (overrides page) + in: query + name: cursor + type: string + - default: 1 + description: Page number, 1-based (ignored if cursor set) + in: query + name: page + type: integer + - default: 20 + description: Items per page (max 100) + in: query + name: limit + type: integer + - description: Filter by creator UUID + in: query + name: user_id + type: string + - description: Filter by genre + in: query + name: genre + type: string + - description: Filter by audio format (mp3, flac, wav, ogg, m4a, aac) + in: query + name: format + type: string + - default: created_at + description: Sort column (created_at, play_count, title) + in: query + name: sort_by + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + pagination: + type: object + tracks: + items: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: array + type: object + type: object + "400": + description: Invalid query params + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + summary: List tracks + tags: + - Track post: consumes: - multipart/form-data @@ -2396,6 +2549,152 @@ paths: summary: Upload Track tags: - Track + /tracks/{id}: + delete: + description: Soft-delete a track (sets deleted_at). Caller must own the track + or be admin. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "403": + description: Not owner / no admin + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Delete track + tags: + - Track + get: + description: Retrieve a single track. Private play_count / like_count are omitted + for non-owners (v0.10.3 F202). + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + track: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + summary: Get track by ID + tags: + - Track + put: + consumes: + - application/json + description: Update the metadata of an existing track. Caller must own the track + or be admin. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Updated metadata + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.UpdateTrackRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + track: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: object + type: object + "400": + description: Validation / invalid id + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "403": + description: Not owner / no admin + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Update track metadata + tags: + - Track /tracks/{id}/comments: get: consumes: @@ -2554,6 +2853,102 @@ paths: summary: Delete comment tags: - Comment + /tracks/{id}/lyrics: + get: + description: Returns the current lyrics for a track, or null if no lyrics exist. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: lyrics may be null + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + lyrics: + type: object + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + summary: Get track lyrics + tags: + - Track + put: + consumes: + - application/json + description: Replace the lyrics of a track. Caller must own the track or be + admin. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Lyrics payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.UpdateLyricsRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + lyrics: + type: object + type: object + type: object + "400": + description: Validation / invalid id + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "403": + description: Not owner / no admin + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Create or update track lyrics + tags: + - Track /tracks/{id}/status: get: consumes: @@ -2597,6 +2992,108 @@ paths: summary: Get Upload Status tags: - Track + /tracks/batch/delete: + post: + consumes: + - application/json + description: Soft-delete up to N tracks in one request. Per-track ownership + checked (admin can delete others). + parameters: + - description: List of track UUIDs + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.BatchDeleteRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + deleted: + items: + type: string + type: array + failed: + items: + type: object + type: array + type: object + type: object + "400": + description: Validation / batch size exceeded + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Batch delete tracks + tags: + - Track + /tracks/batch/update: + post: + consumes: + - application/json + description: Apply a partial metadata update to up to N tracks in one request. + Per-track ownership checked. + parameters: + - description: Track UUIDs + shared updates map + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.BatchUpdateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + failed: + items: + type: object + type: array + updated: + items: + type: string + type: array + type: object + type: object + "400": + description: Validation / batch size exceeded + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Batch update tracks + tags: + - Track /tracks/chunk: post: consumes: @@ -3428,6 +3925,11 @@ paths: tags: - Webhook securityDefinitions: + ApiKeyAuth: + description: 'Developer API key (obtain from Developer Portal). Format: vza_xxxxx' + in: header + name: X-API-Key + type: apiKey BearerAuth: in: header name: Authorization diff --git a/veza-backend-api/internal/core/track/track_crud_handler.go b/veza-backend-api/internal/core/track/track_crud_handler.go index 1d92d1f25..adff5a58e 100644 --- a/veza-backend-api/internal/core/track/track_crud_handler.go +++ b/veza-backend-api/internal/core/track/track_crud_handler.go @@ -54,6 +54,21 @@ type BatchUpdateRequest struct { // ListTracks gère la liste des tracks avec pagination, filtres et tri. // v0.931: Supports cursor-based pagination via ?cursor=xxx&limit=20 for consistent performance. // Falls back to page/limit (offset) when cursor is not provided. +// @Summary List tracks +// @Description List tracks with pagination, filters, sort. Cursor-based when ?cursor provided, otherwise page/limit offset. +// @Tags Track +// @Produce json +// @Param cursor query string false "Opaque pagination cursor (overrides page)" +// @Param page query int false "Page number, 1-based (ignored if cursor set)" default(1) +// @Param limit query int false "Items per page (max 100)" default(20) +// @Param user_id query string false "Filter by creator UUID" +// @Param genre query string false "Filter by genre" +// @Param format query string false "Filter by audio format (mp3, flac, wav, ogg, m4a, aac)" +// @Param sort_by query string false "Sort column (created_at, play_count, title)" default(created_at) +// @Success 200 {object} response.APIResponse{data=object{tracks=[]models.Track,pagination=object}} +// @Failure 400 {object} response.APIResponse "Invalid query params" +// @Failure 500 {object} response.APIResponse "Internal Error" +// @Router /tracks [get] func (h *TrackHandler) ListTracks(c *gin.Context) { cursor := c.Query("cursor") page := c.DefaultQuery("page", "1") @@ -138,6 +153,16 @@ func (h *TrackHandler) ListTracks(c *gin.Context) { } // GetTrack gère la récupération d'un track par ID +// @Summary Get track by ID +// @Description Retrieve a single track. Private play_count / like_count are omitted for non-owners (v0.10.3 F202). +// @Tags Track +// @Produce json +// @Param id path string true "Track UUID" +// @Success 200 {object} response.APIResponse{data=object{track=models.Track}} +// @Failure 400 {object} response.APIResponse "Invalid track id" +// @Failure 404 {object} response.APIResponse "Track not found" +// @Failure 500 {object} response.APIResponse "Internal Error" +// @Router /tracks/{id} [get] func (h *TrackHandler) GetTrack(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { @@ -196,6 +221,21 @@ func hideLikeCountFromTrack(track *models.Track) map[string]interface{} { } // UpdateTrack gère la mise à jour d'un track +// @Summary Update track metadata +// @Description Update the metadata of an existing track. Caller must own the track or be admin. +// @Tags Track +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Param request body UpdateTrackRequest true "Updated metadata" +// @Success 200 {object} response.APIResponse{data=object{track=models.Track}} +// @Failure 400 {object} response.APIResponse "Validation / invalid id" +// @Failure 401 {object} response.APIResponse "Unauthorized" +// @Failure 403 {object} response.APIResponse "Not owner / no admin" +// @Failure 404 {object} response.APIResponse "Track not found" +// @Failure 500 {object} response.APIResponse "Internal Error" +// @Router /tracks/{id} [put] func (h *TrackHandler) UpdateTrack(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { @@ -279,6 +319,16 @@ func (h *TrackHandler) UpdateTrack(c *gin.Context) { } // GetLyrics gère la récupération des paroles d'un track +// @Summary Get track lyrics +// @Description Returns the current lyrics for a track, or null if no lyrics exist. +// @Tags Track +// @Produce json +// @Param id path string true "Track UUID" +// @Success 200 {object} response.APIResponse{data=object{lyrics=object}} "lyrics may be null" +// @Failure 400 {object} response.APIResponse "Invalid track id" +// @Failure 404 {object} response.APIResponse "Track not found" +// @Failure 500 {object} response.APIResponse "Internal Error" +// @Router /tracks/{id}/lyrics [get] func (h *TrackHandler) GetLyrics(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { @@ -311,6 +361,21 @@ func (h *TrackHandler) GetLyrics(c *gin.Context) { } // UpdateLyrics gère la création/mise à jour des paroles +// @Summary Create or update track lyrics +// @Description Replace the lyrics of a track. Caller must own the track or be admin. +// @Tags Track +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Param request body UpdateLyricsRequest true "Lyrics payload" +// @Success 200 {object} response.APIResponse{data=object{lyrics=object}} +// @Failure 400 {object} response.APIResponse "Validation / invalid id" +// @Failure 401 {object} response.APIResponse "Unauthorized" +// @Failure 403 {object} response.APIResponse "Not owner / no admin" +// @Failure 404 {object} response.APIResponse "Track not found" +// @Failure 500 {object} response.APIResponse "Internal Error" +// @Router /tracks/{id}/lyrics [put] func (h *TrackHandler) UpdateLyrics(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { @@ -348,6 +413,19 @@ func (h *TrackHandler) UpdateLyrics(c *gin.Context) { } // DeleteTrack gère la suppression d'un track +// @Summary Delete track +// @Description Soft-delete a track (sets deleted_at). Caller must own the track or be admin. +// @Tags Track +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Success 200 {object} response.APIResponse{data=object{message=string}} +// @Failure 400 {object} response.APIResponse "Invalid track id" +// @Failure 401 {object} response.APIResponse "Unauthorized" +// @Failure 403 {object} response.APIResponse "Not owner / no admin" +// @Failure 404 {object} response.APIResponse "Track not found" +// @Failure 500 {object} response.APIResponse "Internal Error" +// @Router /tracks/{id} [delete] func (h *TrackHandler) DeleteTrack(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { @@ -392,6 +470,18 @@ func (h *TrackHandler) DeleteTrack(c *gin.Context) { } // BatchDeleteTracks gère la suppression en lot de plusieurs tracks +// @Summary Batch delete tracks +// @Description Soft-delete up to N tracks in one request. Per-track ownership checked (admin can delete others). +// @Tags Track +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body BatchDeleteRequest true "List of track UUIDs" +// @Success 200 {object} response.APIResponse{data=object{deleted=[]string,failed=[]object}} +// @Failure 400 {object} response.APIResponse "Validation / batch size exceeded" +// @Failure 401 {object} response.APIResponse "Unauthorized" +// @Failure 500 {object} response.APIResponse "Internal Error" +// @Router /tracks/batch/delete [post] func (h *TrackHandler) BatchDeleteTracks(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { @@ -436,6 +526,18 @@ func (h *TrackHandler) BatchDeleteTracks(c *gin.Context) { } // BatchUpdateTracks gère la mise à jour en lot de plusieurs tracks +// @Summary Batch update tracks +// @Description Apply a partial metadata update to up to N tracks in one request. Per-track ownership checked. +// @Tags Track +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body BatchUpdateRequest true "Track UUIDs + shared updates map" +// @Success 200 {object} response.APIResponse{data=object{updated=[]string,failed=[]object}} +// @Failure 400 {object} response.APIResponse "Validation / batch size exceeded" +// @Failure 401 {object} response.APIResponse "Unauthorized" +// @Failure 500 {object} response.APIResponse "Internal Error" +// @Router /tracks/batch/update [post] func (h *TrackHandler) BatchUpdateTracks(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { diff --git a/veza-backend-api/openapi.yaml b/veza-backend-api/openapi.yaml index d402fef82..37c39ee22 100644 --- a/veza-backend-api/openapi.yaml +++ b/veza-backend-api/openapi.yaml @@ -1,5 +1,29 @@ basePath: /api/v1 definitions: + internal_core_track.BatchDeleteRequest: + properties: + track_ids: + items: + type: string + minItems: 1 + type: array + required: + - track_ids + type: object + internal_core_track.BatchUpdateRequest: + properties: + track_ids: + items: + type: string + minItems: 1 + type: array + updates: + additionalProperties: true + type: object + required: + - track_ids + - updates + type: object internal_core_track.CompleteChunkedUploadRequest: properties: upload_id: @@ -22,6 +46,51 @@ definitions: - total_chunks - total_size type: object + internal_core_track.UpdateLyricsRequest: + properties: + content: + type: string + type: object + internal_core_track.UpdateTrackRequest: + properties: + album: + maxLength: 255 + type: string + artist: + maxLength: 255 + type: string + bpm: + maximum: 300 + minimum: 0 + type: integer + genre: + description: legacy, single + maxLength: 100 + type: string + genres: + description: 'v0.10.1: max 3, taxonomy slugs' + items: + type: string + type: array + is_public: + type: boolean + musical_key: + maxLength: 10 + type: string + tags: + description: 'v0.10.1: max 10, 30 chars each' + items: + type: string + type: array + title: + maxLength: 255 + minLength: 1 + type: string + year: + maximum: 2100 + minimum: 1900 + type: integer + type: object internal_handlers.APIResponse: properties: data: {} @@ -36,8 +105,10 @@ definitions: minLength: 1 type: string parent_id: - description: Changed to *uuid.UUID type: string + timestamp: + description: Position in seconds (0 = top-level, no specific time) + type: number required: - content type: object @@ -178,6 +249,9 @@ definitions: properties: confirm_text: type: string + keep_public_tracks: + description: If true, public tracks remain (attributed to deleted account) + type: boolean password: type: string reason: @@ -468,6 +542,9 @@ definitions: type: string promo_code_id: type: string + refund_deadline: + description: 'v0.12.0: 14-day refund deadline' + type: string status: description: pending, completed, failed, refunded type: string @@ -698,6 +775,12 @@ definitions: type: integer id: type: string + is_default_favorites: + description: v0.10.4 F136 + type: boolean + is_editorial: + description: v0.10.4 F141 + type: boolean is_public: type: boolean title: @@ -794,12 +877,8 @@ definitions: type: string is_public: type: boolean - like_count: - type: integer musical_key: type: string - play_count: - type: integer sample_rate: description: Hz type: integer @@ -880,6 +959,15 @@ definitions: password: description: Virtual field for input type: string + password_changed_at: + description: 'F016: Password expiration tracking' + type: string + promoted_to_creator_at: + description: |- + v1.0.6: set the first time a user self-promotes to `role='creator'` + via POST /api/v1/users/me/upgrade-creator. NULL for users who never + took that path (still 'user', or promoted by an admin out-of-band). + type: string role: type: string slug: @@ -902,7 +990,7 @@ definitions: success: type: boolean type: object -host: localhost:8080 +host: localhost:18080 info: contact: email: support@veza.app @@ -2350,6 +2438,71 @@ paths: tags: - Playlist /tracks: + get: + description: List tracks with pagination, filters, sort. Cursor-based when ?cursor + provided, otherwise page/limit offset. + parameters: + - description: Opaque pagination cursor (overrides page) + in: query + name: cursor + type: string + - default: 1 + description: Page number, 1-based (ignored if cursor set) + in: query + name: page + type: integer + - default: 20 + description: Items per page (max 100) + in: query + name: limit + type: integer + - description: Filter by creator UUID + in: query + name: user_id + type: string + - description: Filter by genre + in: query + name: genre + type: string + - description: Filter by audio format (mp3, flac, wav, ogg, m4a, aac) + in: query + name: format + type: string + - default: created_at + description: Sort column (created_at, play_count, title) + in: query + name: sort_by + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + pagination: + type: object + tracks: + items: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: array + type: object + type: object + "400": + description: Invalid query params + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + summary: List tracks + tags: + - Track post: consumes: - multipart/form-data @@ -2396,6 +2549,152 @@ paths: summary: Upload Track tags: - Track + /tracks/{id}: + delete: + description: Soft-delete a track (sets deleted_at). Caller must own the track + or be admin. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "403": + description: Not owner / no admin + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Delete track + tags: + - Track + get: + description: Retrieve a single track. Private play_count / like_count are omitted + for non-owners (v0.10.3 F202). + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + track: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + summary: Get track by ID + tags: + - Track + put: + consumes: + - application/json + description: Update the metadata of an existing track. Caller must own the track + or be admin. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Updated metadata + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.UpdateTrackRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + track: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: object + type: object + "400": + description: Validation / invalid id + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "403": + description: Not owner / no admin + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Update track metadata + tags: + - Track /tracks/{id}/comments: get: consumes: @@ -2554,6 +2853,102 @@ paths: summary: Delete comment tags: - Comment + /tracks/{id}/lyrics: + get: + description: Returns the current lyrics for a track, or null if no lyrics exist. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: lyrics may be null + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + lyrics: + type: object + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + summary: Get track lyrics + tags: + - Track + put: + consumes: + - application/json + description: Replace the lyrics of a track. Caller must own the track or be + admin. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Lyrics payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.UpdateLyricsRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + lyrics: + type: object + type: object + type: object + "400": + description: Validation / invalid id + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "403": + description: Not owner / no admin + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Create or update track lyrics + tags: + - Track /tracks/{id}/status: get: consumes: @@ -2597,6 +2992,108 @@ paths: summary: Get Upload Status tags: - Track + /tracks/batch/delete: + post: + consumes: + - application/json + description: Soft-delete up to N tracks in one request. Per-track ownership + checked (admin can delete others). + parameters: + - description: List of track UUIDs + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.BatchDeleteRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + deleted: + items: + type: string + type: array + failed: + items: + type: object + type: array + type: object + type: object + "400": + description: Validation / batch size exceeded + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Batch delete tracks + tags: + - Track + /tracks/batch/update: + post: + consumes: + - application/json + description: Apply a partial metadata update to up to N tracks in one request. + Per-track ownership checked. + parameters: + - description: Track UUIDs + shared updates map + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.BatchUpdateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + failed: + items: + type: object + type: array + updated: + items: + type: string + type: array + type: object + type: object + "400": + description: Validation / batch size exceeded + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Batch update tracks + tags: + - Track /tracks/chunk: post: consumes: @@ -3428,14 +3925,13 @@ paths: tags: - Webhook securityDefinitions: - BearerAuth: - description: "JWT Bearer token. Format: Bearer {token}" - in: header - name: Authorization - type: apiKey ApiKeyAuth: - description: "Developer API key. Format: vza_xxxxx (obtain from Developer Portal)" + description: 'Developer API key (obtain from Developer Portal). Format: vza_xxxxx' in: header name: X-API-Key type: apiKey + BearerAuth: + in: header + name: Authorization + type: apiKey swagger: "2.0"