diff --git a/veza-backend-api/docs/docs.go b/veza-backend-api/docs/docs.go index afb89c88c..f3a888f61 100644 --- a/veza-backend-api/docs/docs.go +++ b/veza-backend-api/docs/docs.go @@ -1760,6 +1760,64 @@ const docTemplate = `{ } } }, + "/internal/tracks/{id}/stream-ready": { + "post": { + "description": "Internal endpoint called by the Rust stream server when HLS transcoding completes or fails. Updates the track's stream_status and stream_manifest_url. Requires internal API key (not user-facing).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Stream server callback", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Callback payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.StreamCallbackRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + }, + "400": { + "description": "Validation / invalid id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, "/playlists": { "get": { "security": [ @@ -3015,6 +3073,72 @@ const docTemplate = `{ } } }, + "/tracks/recommendations": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Personalized tracks for D2 autoplay. If seed_track_id is given, returns tracks similar to that seed. Otherwise, uses the caller's history (chronological, no behavioural ranking — CLAUDE.md rule 7).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track recommendations", + "parameters": [ + { + "type": "integer", + "default": 20, + "description": "Max items (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Start from this track's similarity neighbours", + "name": "seed_track_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "tracks": { + "type": "array", + "items": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, "/tracks/resume/{uploadId}": { "get": { "security": [ @@ -3078,6 +3202,361 @@ const docTemplate = `{ } } }, + "/tracks/search": { + "get": { + "description": "Full-text + faceted search on tracks (genre, BPM, duration, tags, musical key, dates). Sort-by and order configurable.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Advanced track search", + "parameters": [ + { + "type": "string", + "description": "Full-text query (title/artist/album)", + "name": "q", + "in": "query" + }, + { + "type": "string", + "description": "Comma-separated tag list", + "name": "tags", + "in": "query" + }, + { + "type": "string", + "default": "OR", + "description": "Tag combinator (OR / AND)", + "name": "tag_mode", + "in": "query" + }, + { + "type": "integer", + "description": "Minimum duration (seconds)", + "name": "min_duration", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum duration (seconds)", + "name": "max_duration", + "in": "query" + }, + { + "type": "integer", + "description": "Minimum BPM", + "name": "min_bpm", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum BPM", + "name": "max_bpm", + "in": "query" + }, + { + "type": "string", + "description": "Genre filter", + "name": "genre", + "in": "query" + }, + { + "type": "string", + "description": "Audio format filter", + "name": "format", + "in": "query" + }, + { + "type": "string", + "description": "Musical key filter", + "name": "musical_key", + "in": "query" + }, + { + "type": "string", + "description": "Created-after (RFC3339)", + "name": "min_date", + "in": "query" + }, + { + "type": "string", + "description": "Created-before (RFC3339)", + "name": "max_date", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Items per page (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "default": "created_at", + "description": "Sort column", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "default": "desc", + "description": "asc / desc", + "name": "sort_order", + "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" + } + } + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, + "/tracks/share/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Permanently disable a share token. Only the share issuer (or admin) can revoke.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Revoke share link", + "parameters": [ + { + "type": "string", + "description": "Share UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "Not issuer", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Share not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/shared/{token}": { + "get": { + "description": "Public endpoint that resolves a share token and returns the track + share metadata. No auth required; the token IS the auth.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track by share token", + "parameters": [ + { + "type": "string", + "description": "Opaque share token issued by CreateShare", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "share": { + "type": "object" + }, + "track": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Missing token", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "Share link expired", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Share or track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/suggested-tags": { + "get": { + "description": "Returns a static tag suggestion list for a genre — useful for upload autocomplete and filter chips.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get suggested tags", + "parameters": [ + { + "type": "string", + "default": "default", + "description": "Genre slug (pop, rock, electronic, hip-hop, jazz, classical, ambient, default)", + "name": "genre", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + ] + } + } + } + } + }, "/tracks/{id}": { "get": { "description": "Retrieve a single track. Private play_count / like_count are omitted for non-owners (v0.10.3 F202).", @@ -3566,6 +4045,381 @@ const docTemplate = `{ } } }, + "/tracks/{id}/download": { + "get": { + "description": "Serve the original audio file. For S3-backed tracks returns a 302 redirect to a signed URL (TTL 30min). For local-backed tracks streams the file with Range support. Public tracks or share_token access; paid tracks require a license.", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "Track" + ], + "summary": "Download a track", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Grants access without authentication for a limited time", + "name": "share_token", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "302": { + "description": "Location header points to signed S3 URL (s3-backed tracks)", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "No permission / license required", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track or file not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/history": { + "get": { + "description": "Paginated audit log of modifications (metadata updates, version changes) for a track.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track history", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 50, + "description": "Items per page", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "history": { + "type": "array", + "items": { + "type": "object" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/like": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Record a like from the authenticated user. Creates a grouped notification for the creator (F554).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Like a 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_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove the authenticated user's like on the track (idempotent).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Unlike a 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_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/likes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns whether the current user has liked the track. The total like count is returned ONLY to the creator or an admin (privacy per ORIGIN_UI_UX_SYSTEM §13).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track like status", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "count is omitted for non-owners", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "is_liked": { + "type": "boolean" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, "/tracks/{id}/lyrics": { "get": { "description": "Returns the current lyrics for a track, or null if no lyrics exist.", @@ -3721,6 +4575,444 @@ const docTemplate = `{ } } }, + "/tracks/{id}/play": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Persist a playback event with optional play_time so the creator's analytics dashboard tracks listening behaviour.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Record play event", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Playback metadata (optional)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_core_track.RecordPlayRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id / body", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/repost": { + "get": { + "description": "Returns whether the current user has reposted the track. Public (optional auth); unauthenticated callers get is_reposted=false.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get repost status", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "is_reposted": { + "type": "boolean" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Add a track to the authenticated user's profile as a repost. Notifies the creator (F204) unless self-repost.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Repost a 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_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove the authenticated user's repost of the track (idempotent).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Remove track repost", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/share": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Generate a tokenized share link for a track with given permission level and optional expiry.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Create share link", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Share parameters", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.CreateShareRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "share": { + "type": "object" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "Not owner", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/stats": { + "get": { + "description": "Aggregated track stats: views, likes, comments, play time, downloads, average duration.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track statistics", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "stats": { + "type": "object" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, "/tracks/{id}/status": { "get": { "security": [ @@ -3793,6 +5085,206 @@ const docTemplate = `{ } } }, + "/tracks/{id}/stream": { + "get": { + "description": "Default playback path. S3-backed tracks return a 302 redirect to a signed URL (TTL 15min). Local-backed tracks are streamed via http.ServeContent with Range support. Always available, unlike /hls/* which is gated by HLSEnabled.", + "produces": [ + "audio/*" + ], + "tags": [ + "Track" + ], + "summary": "Stream a track (raw audio + Range)", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Grants access without authentication", + "name": "share_token", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "302": { + "description": "Location header points to signed S3 URL (s3-backed tracks)", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "No permission", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track or file not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/versions/{versionId}/restore": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Rollback a track to a previous version. Only the track owner can restore.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Restore track version", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Version UUID", + "name": "versionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "Not owner", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track or version not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/waveform": { + "get": { + "description": "Returns a JSON peaks array used by the client to draw the audio waveform preview. 404 if waveform extraction is not complete yet.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track waveform", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Waveform peaks JSON (tool-specific shape)", + "schema": { + "type": "object" + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Waveform not generated / track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, "/users": { "get": { "description": "Get a paginated list of users with optional filtering", @@ -4310,6 +5802,185 @@ const docTemplate = `{ } } }, + "/users/{id}/likes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns paginated tracks the given user has liked. Used for profile \"Likes\" tab.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List tracks liked by a user", + "parameters": [ + { + "type": "string", + "description": "User UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "Items per page (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "tracks": { + "type": "array", + "items": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/users/{id}/reposts": { + "get": { + "description": "Returns paginated tracks the user has reposted. Used for profile \"Reposts\" tab.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List tracks reposted by a user", + "parameters": [ + { + "type": "string", + "description": "User UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "Items per page (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "tracks": { + "type": "array", + "items": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, "/validate": { "post": { "description": "Validates request data against known DTO types without executing the actual operation", @@ -4809,6 +6480,25 @@ const docTemplate = `{ } } }, + "internal_core_track.CreateShareRequest": { + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "expires_at": { + "type": "string" + }, + "permissions": { + "type": "string", + "enum": [ + "read", + "write", + "admin" + ] + } + } + }, "internal_core_track.InitiateChunkedUploadRequest": { "type": "object", "required": [ @@ -4830,6 +6520,37 @@ const docTemplate = `{ } } }, + "internal_core_track.RecordPlayRequest": { + "type": "object", + "properties": { + "play_time": { + "type": "integer", + "minimum": 0 + } + } + }, + "internal_core_track.StreamCallbackRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "error": { + "type": "string" + }, + "manifest_url": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "completed", + "failed", + "processing" + ] + } + } + }, "internal_core_track.UpdateLyricsRequest": { "type": "object", "properties": { @@ -5859,6 +7580,16 @@ const docTemplate = `{ } } }, + "veza-backend-api_internal_handlers.APIResponse": { + "type": "object", + "properties": { + "data": {}, + "error": {}, + "success": { + "type": "boolean" + } + } + }, "veza-backend-api_internal_models.Playlist": { "type": "object", "properties": { diff --git a/veza-backend-api/docs/swagger.json b/veza-backend-api/docs/swagger.json index f1f3d85d0..1eaa98948 100644 --- a/veza-backend-api/docs/swagger.json +++ b/veza-backend-api/docs/swagger.json @@ -1754,6 +1754,64 @@ } } }, + "/internal/tracks/{id}/stream-ready": { + "post": { + "description": "Internal endpoint called by the Rust stream server when HLS transcoding completes or fails. Updates the track's stream_status and stream_manifest_url. Requires internal API key (not user-facing).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Stream server callback", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Callback payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.StreamCallbackRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + }, + "400": { + "description": "Validation / invalid id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, "/playlists": { "get": { "security": [ @@ -3009,6 +3067,72 @@ } } }, + "/tracks/recommendations": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Personalized tracks for D2 autoplay. If seed_track_id is given, returns tracks similar to that seed. Otherwise, uses the caller's history (chronological, no behavioural ranking — CLAUDE.md rule 7).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track recommendations", + "parameters": [ + { + "type": "integer", + "default": 20, + "description": "Max items (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Start from this track's similarity neighbours", + "name": "seed_track_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "tracks": { + "type": "array", + "items": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, "/tracks/resume/{uploadId}": { "get": { "security": [ @@ -3072,6 +3196,361 @@ } } }, + "/tracks/search": { + "get": { + "description": "Full-text + faceted search on tracks (genre, BPM, duration, tags, musical key, dates). Sort-by and order configurable.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Advanced track search", + "parameters": [ + { + "type": "string", + "description": "Full-text query (title/artist/album)", + "name": "q", + "in": "query" + }, + { + "type": "string", + "description": "Comma-separated tag list", + "name": "tags", + "in": "query" + }, + { + "type": "string", + "default": "OR", + "description": "Tag combinator (OR / AND)", + "name": "tag_mode", + "in": "query" + }, + { + "type": "integer", + "description": "Minimum duration (seconds)", + "name": "min_duration", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum duration (seconds)", + "name": "max_duration", + "in": "query" + }, + { + "type": "integer", + "description": "Minimum BPM", + "name": "min_bpm", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum BPM", + "name": "max_bpm", + "in": "query" + }, + { + "type": "string", + "description": "Genre filter", + "name": "genre", + "in": "query" + }, + { + "type": "string", + "description": "Audio format filter", + "name": "format", + "in": "query" + }, + { + "type": "string", + "description": "Musical key filter", + "name": "musical_key", + "in": "query" + }, + { + "type": "string", + "description": "Created-after (RFC3339)", + "name": "min_date", + "in": "query" + }, + { + "type": "string", + "description": "Created-before (RFC3339)", + "name": "max_date", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Items per page (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "default": "created_at", + "description": "Sort column", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "default": "desc", + "description": "asc / desc", + "name": "sort_order", + "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" + } + } + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + } + } + } + } + }, + "/tracks/share/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Permanently disable a share token. Only the share issuer (or admin) can revoke.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Revoke share link", + "parameters": [ + { + "type": "string", + "description": "Share UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "Not issuer", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Share not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/shared/{token}": { + "get": { + "description": "Public endpoint that resolves a share token and returns the track + share metadata. No auth required; the token IS the auth.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track by share token", + "parameters": [ + { + "type": "string", + "description": "Opaque share token issued by CreateShare", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "share": { + "type": "object" + }, + "track": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Missing token", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "Share link expired", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Share or track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/suggested-tags": { + "get": { + "description": "Returns a static tag suggestion list for a genre — useful for upload autocomplete and filter chips.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get suggested tags", + "parameters": [ + { + "type": "string", + "default": "default", + "description": "Genre slug (pop, rock, electronic, hip-hop, jazz, classical, ambient, default)", + "name": "genre", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + ] + } + } + } + } + }, "/tracks/{id}": { "get": { "description": "Retrieve a single track. Private play_count / like_count are omitted for non-owners (v0.10.3 F202).", @@ -3560,6 +4039,381 @@ } } }, + "/tracks/{id}/download": { + "get": { + "description": "Serve the original audio file. For S3-backed tracks returns a 302 redirect to a signed URL (TTL 30min). For local-backed tracks streams the file with Range support. Public tracks or share_token access; paid tracks require a license.", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "Track" + ], + "summary": "Download a track", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Grants access without authentication for a limited time", + "name": "share_token", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "302": { + "description": "Location header points to signed S3 URL (s3-backed tracks)", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "No permission / license required", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track or file not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/history": { + "get": { + "description": "Paginated audit log of modifications (metadata updates, version changes) for a track.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track history", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 50, + "description": "Items per page", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "history": { + "type": "array", + "items": { + "type": "object" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/like": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Record a like from the authenticated user. Creates a grouped notification for the creator (F554).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Like a 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_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove the authenticated user's like on the track (idempotent).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Unlike a 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_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/likes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns whether the current user has liked the track. The total like count is returned ONLY to the creator or an admin (privacy per ORIGIN_UI_UX_SYSTEM §13).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track like status", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "count is omitted for non-owners", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "is_liked": { + "type": "boolean" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, "/tracks/{id}/lyrics": { "get": { "description": "Returns the current lyrics for a track, or null if no lyrics exist.", @@ -3715,6 +4569,444 @@ } } }, + "/tracks/{id}/play": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Persist a playback event with optional play_time so the creator's analytics dashboard tracks listening behaviour.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Record play event", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Playback metadata (optional)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_core_track.RecordPlayRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id / body", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/repost": { + "get": { + "description": "Returns whether the current user has reposted the track. Public (optional auth); unauthenticated callers get is_reposted=false.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get repost status", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "is_reposted": { + "type": "boolean" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Add a track to the authenticated user's profile as a repost. Notifies the creator (F204) unless self-repost.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Repost a 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_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove the authenticated user's repost of the track (idempotent).", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Remove track repost", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/share": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Generate a tokenized share link for a track with given permission level and optional expiry.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Create share link", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Share parameters", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_core_track.CreateShareRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "share": { + "type": "object" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "Not owner", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/stats": { + "get": { + "description": "Aggregated track stats: views, likes, comments, play time, downloads, average duration.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track statistics", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "stats": { + "type": "object" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, "/tracks/{id}/status": { "get": { "security": [ @@ -3787,6 +5079,206 @@ } } }, + "/tracks/{id}/stream": { + "get": { + "description": "Default playback path. S3-backed tracks return a 302 redirect to a signed URL (TTL 15min). Local-backed tracks are streamed via http.ServeContent with Range support. Always available, unlike /hls/* which is gated by HLSEnabled.", + "produces": [ + "audio/*" + ], + "tags": [ + "Track" + ], + "summary": "Stream a track (raw audio + Range)", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Grants access without authentication", + "name": "share_token", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "302": { + "description": "Location header points to signed S3 URL (s3-backed tracks)", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "No permission", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track or file not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/versions/{versionId}/restore": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Rollback a track to a previous version. Only the track owner can restore.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Restore track version", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Version UUID", + "name": "versionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "403": { + "description": "Not owner", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Track or version not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/tracks/{id}/waveform": { + "get": { + "description": "Returns a JSON peaks array used by the client to draw the audio waveform preview. 404 if waveform extraction is not complete yet.", + "produces": [ + "application/json" + ], + "tags": [ + "Track" + ], + "summary": "Get track waveform", + "parameters": [ + { + "type": "string", + "description": "Track UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Waveform peaks JSON (tool-specific shape)", + "schema": { + "type": "object" + } + }, + "400": { + "description": "Invalid track id", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "404": { + "description": "Waveform not generated / track not found", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, "/users": { "get": { "description": "Get a paginated list of users with optional filtering", @@ -4304,6 +5796,185 @@ } } }, + "/users/{id}/likes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns paginated tracks the given user has liked. Used for profile \"Likes\" tab.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List tracks liked by a user", + "parameters": [ + { + "type": "string", + "description": "User UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "Items per page (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "tracks": { + "type": "array", + "items": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, + "/users/{id}/reposts": { + "get": { + "description": "Returns paginated tracks the user has reposted. Used for profile \"Reposts\" tab.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List tracks reposted by a user", + "parameters": [ + { + "type": "string", + "description": "User UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "Items per page (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "tracks": { + "type": "array", + "items": { + "$ref": "#/definitions/veza-backend-api_internal_models.Track" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Validation", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/veza-backend-api_internal_handlers.APIResponse" + } + } + } + } + }, "/validate": { "post": { "description": "Validates request data against known DTO types without executing the actual operation", @@ -4803,6 +6474,25 @@ } } }, + "internal_core_track.CreateShareRequest": { + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "expires_at": { + "type": "string" + }, + "permissions": { + "type": "string", + "enum": [ + "read", + "write", + "admin" + ] + } + } + }, "internal_core_track.InitiateChunkedUploadRequest": { "type": "object", "required": [ @@ -4824,6 +6514,37 @@ } } }, + "internal_core_track.RecordPlayRequest": { + "type": "object", + "properties": { + "play_time": { + "type": "integer", + "minimum": 0 + } + } + }, + "internal_core_track.StreamCallbackRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "error": { + "type": "string" + }, + "manifest_url": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "completed", + "failed", + "processing" + ] + } + } + }, "internal_core_track.UpdateLyricsRequest": { "type": "object", "properties": { @@ -5853,6 +7574,16 @@ } } }, + "veza-backend-api_internal_handlers.APIResponse": { + "type": "object", + "properties": { + "data": {}, + "error": {}, + "success": { + "type": "boolean" + } + } + }, "veza-backend-api_internal_models.Playlist": { "type": "object", "properties": { diff --git a/veza-backend-api/docs/swagger.yaml b/veza-backend-api/docs/swagger.yaml index 37c39ee22..d4475668d 100644 --- a/veza-backend-api/docs/swagger.yaml +++ b/veza-backend-api/docs/swagger.yaml @@ -31,6 +31,19 @@ definitions: required: - upload_id type: object + internal_core_track.CreateShareRequest: + properties: + expires_at: + type: string + permissions: + enum: + - read + - write + - admin + type: string + required: + - permissions + type: object internal_core_track.InitiateChunkedUploadRequest: properties: filename: @@ -46,6 +59,27 @@ definitions: - total_chunks - total_size type: object + internal_core_track.RecordPlayRequest: + properties: + play_time: + minimum: 0 + type: integer + type: object + internal_core_track.StreamCallbackRequest: + properties: + error: + type: string + manifest_url: + type: string + status: + enum: + - completed + - failed + - processing + type: string + required: + - status + type: object internal_core_track.UpdateLyricsRequest: properties: content: @@ -759,6 +793,13 @@ definitions: value: type: string type: object + veza-backend-api_internal_handlers.APIResponse: + properties: + data: {} + error: {} + success: + type: boolean + type: object veza-backend-api_internal_models.Playlist: properties: collaborators: @@ -2084,6 +2125,46 @@ paths: summary: Get comment replies tags: - Comment + /internal/tracks/{id}/stream-ready: + post: + consumes: + - application/json + description: Internal endpoint called by the Rust stream server when HLS transcoding + completes or fails. Updates the track's stream_status and stream_manifest_url. + Requires internal API key (not user-facing). + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Callback payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.StreamCallbackRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + properties: + message: + type: string + type: object + "400": + description: Validation / invalid id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Stream server callback + tags: + - Track /playlists: get: consumes: @@ -2853,6 +2934,245 @@ paths: summary: Delete comment tags: - Comment + /tracks/{id}/download: + get: + description: Serve the original audio file. For S3-backed tracks returns a 302 + redirect to a signed URL (TTL 30min). For local-backed tracks streams the + file with Range support. Public tracks or share_token access; paid tracks + require a license. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Grants access without authentication for a limited time + in: query + name: share_token + type: string + produces: + - application/octet-stream + responses: + "200": + description: OK + schema: + type: file + "302": + description: Location header points to signed S3 URL (s3-backed tracks) + schema: + type: string + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: No permission / license required + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track or file not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Download a track + tags: + - Track + /tracks/{id}/history: + get: + description: Paginated audit log of modifications (metadata updates, version + changes) for a track. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - default: 50 + description: Items per page + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + history: + items: + type: object + type: array + limit: + type: integer + offset: + type: integer + total: + type: integer + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Get track history + tags: + - Track + /tracks/{id}/like: + delete: + description: Remove the authenticated user's like on the track (idempotent). + 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_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Unlike a track + tags: + - Track + post: + description: Record a like from the authenticated user. Creates a grouped notification + for the creator (F554). + 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_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Like a track + tags: + - Track + /tracks/{id}/likes: + get: + description: Returns whether the current user has liked the track. The total + like count is returned ONLY to the creator or an admin (privacy per ORIGIN_UI_UX_SYSTEM + §13). + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: count is omitted for non-owners + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + count: + type: integer + is_liked: + type: boolean + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Get track like status + tags: + - Track /tracks/{id}/lyrics: get: description: Returns the current lyrics for a track, or null if no lyrics exist. @@ -2949,6 +3269,276 @@ paths: summary: Create or update track lyrics tags: - Track + /tracks/{id}/play: + post: + consumes: + - application/json + description: Persist a playback event with optional play_time so the creator's + analytics dashboard tracks listening behaviour. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Playback metadata (optional) + in: body + name: request + schema: + $ref: '#/definitions/internal_core_track.RecordPlayRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + id: + type: string + message: + type: string + type: object + type: object + "400": + description: Invalid track id / body + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Record play event + tags: + - Track + /tracks/{id}/repost: + delete: + description: Remove the authenticated user's repost of the track (idempotent). + 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_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Remove track repost + tags: + - Track + get: + description: Returns whether the current user has reposted the track. Public + (optional auth); unauthenticated callers get is_reposted=false. + 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_handlers.APIResponse' + - properties: + data: + properties: + is_reposted: + type: boolean + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Get repost status + tags: + - Track + post: + description: Add a track to the authenticated user's profile as a repost. Notifies + the creator (F204) unless self-repost. + 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_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Repost a track + tags: + - Track + /tracks/{id}/share: + post: + consumes: + - application/json + description: Generate a tokenized share link for a track with given permission + level and optional expiry. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Share parameters + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.CreateShareRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + share: + type: object + type: object + type: object + "400": + description: Validation + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: Not owner + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Create share link + tags: + - Track + /tracks/{id}/stats: + get: + description: 'Aggregated track stats: views, likes, comments, play time, downloads, + average duration.' + 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_handlers.APIResponse' + - properties: + data: + properties: + stats: + type: object + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Get track statistics + tags: + - Track /tracks/{id}/status: get: consumes: @@ -2992,6 +3582,138 @@ paths: summary: Get Upload Status tags: - Track + /tracks/{id}/stream: + get: + description: Default playback path. S3-backed tracks return a 302 redirect to + a signed URL (TTL 15min). Local-backed tracks are streamed via http.ServeContent + with Range support. Always available, unlike /hls/* which is gated by HLSEnabled. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Grants access without authentication + in: query + name: share_token + type: string + produces: + - audio/* + responses: + "200": + description: OK + schema: + type: file + "302": + description: Location header points to signed S3 URL (s3-backed tracks) + schema: + type: string + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: No permission + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track or file not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Stream a track (raw audio + Range) + tags: + - Track + /tracks/{id}/versions/{versionId}/restore: + post: + description: Rollback a track to a previous version. Only the track owner can + restore. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Version UUID + in: path + name: versionId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: Not owner + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track or version not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Restore track version + tags: + - Track + /tracks/{id}/waveform: + get: + description: Returns a JSON peaks array used by the client to draw the audio + waveform preview. 404 if waveform extraction is not complete yet. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Waveform peaks JSON (tool-specific shape) + schema: + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Waveform not generated / track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Get track waveform + tags: + - Track /tracks/batch/delete: post: consumes: @@ -3291,6 +4013,47 @@ paths: summary: Get Upload Quota tags: - Track + /tracks/recommendations: + get: + description: Personalized tracks for D2 autoplay. If seed_track_id is given, + returns tracks similar to that seed. Otherwise, uses the caller's history + (chronological, no behavioural ranking — CLAUDE.md rule 7). + parameters: + - default: 20 + description: Max items (max 100) + in: query + name: limit + type: integer + - description: Start from this track's similarity neighbours + in: query + name: seed_track_id + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + tracks: + items: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: array + type: object + type: object + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Get track recommendations + tags: + - Track /tracks/resume/{uploadId}: get: consumes: @@ -3328,6 +4091,233 @@ paths: summary: Resume Upload tags: - Track + /tracks/search: + get: + description: Full-text + faceted search on tracks (genre, BPM, duration, tags, + musical key, dates). Sort-by and order configurable. + parameters: + - description: Full-text query (title/artist/album) + in: query + name: q + type: string + - description: Comma-separated tag list + in: query + name: tags + type: string + - default: OR + description: Tag combinator (OR / AND) + in: query + name: tag_mode + type: string + - description: Minimum duration (seconds) + in: query + name: min_duration + type: integer + - description: Maximum duration (seconds) + in: query + name: max_duration + type: integer + - description: Minimum BPM + in: query + name: min_bpm + type: integer + - description: Maximum BPM + in: query + name: max_bpm + type: integer + - description: Genre filter + in: query + name: genre + type: string + - description: Audio format filter + in: query + name: format + type: string + - description: Musical key filter + in: query + name: musical_key + type: string + - description: Created-after (RFC3339) + in: query + name: min_date + type: string + - description: Created-before (RFC3339) + in: query + name: max_date + type: string + - default: 1 + description: Page (1-based) + in: query + name: page + type: integer + - default: 20 + description: Items per page (max 100) + in: query + name: limit + type: integer + - default: created_at + description: Sort column + in: query + name: sort_by + type: string + - default: desc + description: asc / desc + in: query + name: sort_order + 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 + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + summary: Advanced track search + tags: + - Track + /tracks/share/{id}: + delete: + description: Permanently disable a share token. Only the share issuer (or admin) + can revoke. + parameters: + - description: Share UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Validation + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: Not issuer + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Share not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Revoke share link + tags: + - Track + /tracks/shared/{token}: + get: + description: Public endpoint that resolves a share token and returns the track + + share metadata. No auth required; the token IS the auth. + parameters: + - description: Opaque share token issued by CreateShare + in: path + name: token + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + share: + type: object + track: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: object + type: object + "400": + description: Missing token + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: Share link expired + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Share or track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Get track by share token + tags: + - Track + /tracks/suggested-tags: + get: + description: Returns a static tag suggestion list for a genre — useful for upload + autocomplete and filter chips. + parameters: + - default: default + description: Genre slug (pop, rock, electronic, hip-hop, jazz, classical, + ambient, default) + in: query + name: genre + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + tags: + items: + type: string + type: array + type: object + type: object + summary: Get suggested tags + tags: + - Track /users: get: consumes: @@ -3555,6 +4545,120 @@ paths: summary: Get Profile Completion tags: - User + /users/{id}/likes: + get: + description: Returns paginated tracks the given user has liked. Used for profile + "Likes" tab. + parameters: + - description: User UUID + in: path + name: id + required: true + type: string + - default: 20 + description: Items per page (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset for pagination + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + limit: + type: integer + offset: + type: integer + total: + type: integer + tracks: + items: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: array + type: object + type: object + "400": + description: Validation + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: List tracks liked by a user + tags: + - User + /users/{id}/reposts: + get: + description: Returns paginated tracks the user has reposted. Used for profile + "Reposts" tab. + parameters: + - description: User UUID + in: path + name: id + required: true + type: string + - default: 20 + description: Items per page (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset for pagination + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + limit: + type: integer + offset: + type: integer + total: + type: integer + tracks: + items: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: array + type: object + type: object + "400": + description: Validation + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: List tracks reposted by a user + tags: + - User /users/by-username/{username}: get: consumes: diff --git a/veza-backend-api/internal/core/track/track_analytics_handler.go b/veza-backend-api/internal/core/track/track_analytics_handler.go index 75e4f2cf1..a85899ac9 100644 --- a/veza-backend-api/internal/core/track/track_analytics_handler.go +++ b/veza-backend-api/internal/core/track/track_analytics_handler.go @@ -21,6 +21,16 @@ type RecordPlayRequest struct { } // GetTrackStats returns track statistics (plays, likes, views, etc.) +// @Summary Get track statistics +// @Description Aggregated track stats: views, likes, comments, play time, downloads, average duration. +// @Tags Track +// @Produce json +// @Param id path string true "Track UUID" +// @Success 200 {object} handlers.APIResponse{data=object{stats=object}} +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Failure 404 {object} handlers.APIResponse "Track not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/stats [get] func (h *TrackHandler) GetTrackStats(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { @@ -62,6 +72,18 @@ func (h *TrackHandler) GetTrackStats(c *gin.Context) { } // GetTrackHistory returns modification history for a track +// @Summary Get track history +// @Description Paginated audit log of modifications (metadata updates, version changes) for a track. +// @Tags Track +// @Produce json +// @Param id path string true "Track UUID" +// @Param limit query int false "Items per page" default(50) +// @Param offset query int false "Offset" default(0) +// @Success 200 {object} handlers.APIResponse{data=object{history=[]object,total=integer,limit=integer,offset=integer}} +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Failure 404 {object} handlers.APIResponse "Track not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/history [get] func (h *TrackHandler) GetTrackHistory(c *gin.Context) { if h.historyService == nil { h.respondWithError(c, http.StatusInternalServerError, "history service not available") @@ -124,6 +146,20 @@ func (h *TrackHandler) GetTrackHistory(c *gin.Context) { } // RecordPlay enregistre un événement de lecture pour un track +// @Summary Record play event +// @Description Persist a playback event with optional play_time so the creator's analytics dashboard tracks listening behaviour. +// @Tags Track +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Param request body RecordPlayRequest false "Playback metadata (optional)" +// @Success 200 {object} handlers.APIResponse{data=object{message=string,id=string}} +// @Failure 400 {object} handlers.APIResponse "Invalid track id / body" +// @Failure 401 {object} handlers.APIResponse "Unauthorized" +// @Failure 404 {object} handlers.APIResponse "Track not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/play [post] func (h *TrackHandler) RecordPlay(c *gin.Context) { if h.playbackAnalyticsService == nil { h.respondWithError(c, http.StatusInternalServerError, "playback analytics service not available") @@ -183,6 +219,20 @@ func (h *TrackHandler) RecordPlay(c *gin.Context) { } // RestoreVersion restaure une version spécifique d'un track +// @Summary Restore track version +// @Description Rollback a track to a previous version. Only the track owner can restore. +// @Tags Track +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Param versionId path string true "Version UUID" +// @Success 200 {object} handlers.APIResponse{data=object{message=string}} +// @Failure 400 {object} handlers.APIResponse "Invalid id" +// @Failure 401 {object} handlers.APIResponse "Unauthorized" +// @Failure 403 {object} handlers.APIResponse "Not owner" +// @Failure 404 {object} handlers.APIResponse "Track or version not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/versions/{versionId}/restore [post] func (h *TrackHandler) RestoreVersion(c *gin.Context) { if h.versionService == nil { h.respondWithError(c, http.StatusInternalServerError, "version service not available") diff --git a/veza-backend-api/internal/core/track/track_hls_handler.go b/veza-backend-api/internal/core/track/track_hls_handler.go index 5275962be..f8d9779b9 100644 --- a/veza-backend-api/internal/core/track/track_hls_handler.go +++ b/veza-backend-api/internal/core/track/track_hls_handler.go @@ -11,6 +11,9 @@ import ( "github.com/google/uuid" "veza-backend-api/internal/common" + // handlers is imported to let swaggo resolve handlers.APIResponse refs in + // doc comments (@Failure / @Success); not called directly from this file. + _ "veza-backend-api/internal/handlers" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" @@ -26,6 +29,17 @@ type StreamCallbackRequest struct { } // HandleStreamCallback handles the callback from stream server +// @Summary Stream server callback +// @Description Internal endpoint called by the Rust stream server when HLS transcoding completes or fails. Updates the track's stream_status and stream_manifest_url. Requires internal API key (not user-facing). +// @Tags Track +// @Accept json +// @Produce json +// @Param id path string true "Track UUID" +// @Param request body StreamCallbackRequest true "Callback payload" +// @Success 200 {object} object{message=string} +// @Failure 400 {object} handlers.APIResponse "Validation / invalid id" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /internal/tracks/{id}/stream-ready [post] func (h *TrackHandler) HandleStreamCallback(c *gin.Context) { trackIDStr := c.Param("id") // MIGRATION UUID: TrackID is UUID @@ -52,6 +66,19 @@ func (h *TrackHandler) HandleStreamCallback(c *gin.Context) { } // DownloadTrack gère le téléchargement d'un track +// @Summary Download a track +// @Description Serve the original audio file. For S3-backed tracks returns a 302 redirect to a signed URL (TTL 30min). For local-backed tracks streams the file with Range support. Public tracks or share_token access; paid tracks require a license. +// @Tags Track +// @Produce application/octet-stream +// @Param id path string true "Track UUID" +// @Param share_token query string false "Grants access without authentication for a limited time" +// @Success 200 {file} binary +// @Success 302 {string} string "Location header points to signed S3 URL (s3-backed tracks)" +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Failure 403 {object} handlers.APIResponse "No permission / license required" +// @Failure 404 {object} handlers.APIResponse "Track or file not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/download [get] func (h *TrackHandler) DownloadTrack(c *gin.Context) { // Récupérer l'utilisateur s'il est authentifié var userID uuid.UUID @@ -193,6 +220,19 @@ func (h *TrackHandler) DownloadTrack(c *gin.Context) { // by HLSEnabled), /stream is always available and is the default playback path when // HLS transcoding is off. The file is served via http.ServeContent which handles // Range, If-Modified-Since and If-None-Match automatically. +// @Summary Stream a track (raw audio + Range) +// @Description Default playback path. S3-backed tracks return a 302 redirect to a signed URL (TTL 15min). Local-backed tracks are streamed via http.ServeContent with Range support. Always available, unlike /hls/* which is gated by HLSEnabled. +// @Tags Track +// @Produce audio/* +// @Param id path string true "Track UUID" +// @Param share_token query string false "Grants access without authentication" +// @Success 200 {file} binary +// @Success 302 {string} string "Location header points to signed S3 URL (s3-backed tracks)" +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Failure 403 {object} handlers.APIResponse "No permission" +// @Failure 404 {object} handlers.APIResponse "Track or file not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/stream [get] func (h *TrackHandler) StreamTrack(c *gin.Context) { var userID uuid.UUID if userIDInterface, exists := c.Get("user_id"); exists { diff --git a/veza-backend-api/internal/core/track/track_search_handler.go b/veza-backend-api/internal/core/track/track_search_handler.go index a8754dda1..df1820171 100644 --- a/veza-backend-api/internal/core/track/track_search_handler.go +++ b/veza-backend-api/internal/core/track/track_search_handler.go @@ -29,6 +29,16 @@ var tagSuggestionsByGenre = map[string][]string{ } // GetRecommendations returns personalized track recommendations (D2 autoplay) +// @Summary Get track recommendations +// @Description Personalized tracks for D2 autoplay. If seed_track_id is given, returns tracks similar to that seed. Otherwise, uses the caller's history (chronological, no behavioural ranking — CLAUDE.md rule 7). +// @Tags Track +// @Produce json +// @Security BearerAuth +// @Param limit query int false "Max items (max 100)" default(20) +// @Param seed_track_id query string false "Start from this track's similarity neighbours" +// @Success 200 {object} response.APIResponse{data=object{tracks=[]models.Track}} +// @Failure 500 {object} response.APIResponse "Internal Error" +// @Router /tracks/recommendations [get] func (h *TrackHandler) GetRecommendations(c *gin.Context) { if h.trackRecommendationService == nil { response.InternalServerError(c, "recommendations unavailable") @@ -72,6 +82,13 @@ func (h *TrackHandler) GetRecommendations(c *gin.Context) { } // GetSuggestedTags returns tag suggestions based on genre and BPM (E4) +// @Summary Get suggested tags +// @Description Returns a static tag suggestion list for a genre — useful for upload autocomplete and filter chips. +// @Tags Track +// @Produce json +// @Param genre query string false "Genre slug (pop, rock, electronic, hip-hop, jazz, classical, ambient, default)" default(default) +// @Success 200 {object} response.APIResponse{data=object{tags=[]string}} +// @Router /tracks/suggested-tags [get] func (h *TrackHandler) GetSuggestedTags(c *gin.Context) { genre := strings.ToLower(strings.TrimSpace(c.DefaultQuery("genre", ""))) if genre == "" { @@ -85,6 +102,29 @@ func (h *TrackHandler) GetSuggestedTags(c *gin.Context) { } // SearchTracks gère la recherche avancée de tracks +// @Summary Advanced track search +// @Description Full-text + faceted search on tracks (genre, BPM, duration, tags, musical key, dates). Sort-by and order configurable. +// @Tags Track +// @Produce json +// @Param q query string false "Full-text query (title/artist/album)" +// @Param tags query string false "Comma-separated tag list" +// @Param tag_mode query string false "Tag combinator (OR / AND)" default(OR) +// @Param min_duration query int false "Minimum duration (seconds)" +// @Param max_duration query int false "Maximum duration (seconds)" +// @Param min_bpm query int false "Minimum BPM" +// @Param max_bpm query int false "Maximum BPM" +// @Param genre query string false "Genre filter" +// @Param format query string false "Audio format filter" +// @Param musical_key query string false "Musical key filter" +// @Param min_date query string false "Created-after (RFC3339)" +// @Param max_date query string false "Created-before (RFC3339)" +// @Param page query int false "Page (1-based)" default(1) +// @Param limit query int false "Items per page (max 100)" default(20) +// @Param sort_by query string false "Sort column" default(created_at) +// @Param sort_order query string false "asc / desc" default(desc) +// @Success 200 {object} response.APIResponse{data=object{tracks=[]models.Track,pagination=object}} +// @Failure 500 {object} response.APIResponse "Internal Error" +// @Router /tracks/search [get] func (h *TrackHandler) SearchTracks(c *gin.Context) { if h.searchService == nil { h.respondWithError(c, http.StatusInternalServerError, "search service not available") diff --git a/veza-backend-api/internal/core/track/track_social_handler.go b/veza-backend-api/internal/core/track/track_social_handler.go index b828de8f4..4271d986c 100644 --- a/veza-backend-api/internal/core/track/track_social_handler.go +++ b/veza-backend-api/internal/core/track/track_social_handler.go @@ -25,6 +25,18 @@ type CreateShareRequest struct { } // LikeTrack gère l'ajout d'un like sur un track +// @Summary Like a track +// @Description Record a like from the authenticated user. Creates a grouped notification for the creator (F554). +// @Tags Track +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Success 200 {object} handlers.APIResponse{data=object{message=string}} +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Failure 401 {object} handlers.APIResponse "Unauthorized" +// @Failure 404 {object} handlers.APIResponse "Track not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/like [post] func (h *TrackHandler) LikeTrack(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { @@ -67,6 +79,17 @@ func (h *TrackHandler) LikeTrack(c *gin.Context) { } // UnlikeTrack gère la suppression d'un like sur un track +// @Summary Unlike a track +// @Description Remove the authenticated user's like on the track (idempotent). +// @Tags Track +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Success 200 {object} handlers.APIResponse{data=object{message=string}} +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Failure 401 {object} handlers.APIResponse "Unauthorized" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/like [delete] func (h *TrackHandler) UnlikeTrack(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { @@ -95,6 +118,18 @@ func (h *TrackHandler) UnlikeTrack(c *gin.Context) { // GetTrackLikes gère la récupération du nombre de likes d'un track. // v0.10.3 F202: count visible only by track creator (or admin); others get is_liked only. +// @Summary Get track like status +// @Description Returns whether the current user has liked the track. The total like count is returned ONLY to the creator or an admin (privacy per ORIGIN_UI_UX_SYSTEM §13). +// @Tags Track +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Success 200 {object} handlers.APIResponse{data=object{is_liked=boolean,count=integer}} "count is omitted for non-owners" +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Failure 401 {object} handlers.APIResponse "Unauthorized" +// @Failure 404 {object} handlers.APIResponse "Track not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/likes [get] func (h *TrackHandler) GetTrackLikes(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { @@ -137,6 +172,19 @@ func (h *TrackHandler) GetTrackLikes(c *gin.Context) { } // GetUserLikedTracks gère la récupération des tracks likés par un utilisateur +// @Summary List tracks liked by a user +// @Description Returns paginated tracks the given user has liked. Used for profile "Likes" tab. +// @Tags User +// @Produce json +// @Security BearerAuth +// @Param id path string true "User UUID" +// @Param limit query int false "Items per page (max 100)" default(20) +// @Param offset query int false "Offset for pagination" default(0) +// @Success 200 {object} handlers.APIResponse{data=object{tracks=[]models.Track,total=integer,limit=integer,offset=integer}} +// @Failure 400 {object} handlers.APIResponse "Validation" +// @Failure 401 {object} handlers.APIResponse "Unauthorized" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /users/{id}/likes [get] func (h *TrackHandler) GetUserLikedTracks(c *gin.Context) { userIDStr := c.Param("id") if userIDStr == "" { @@ -188,6 +236,17 @@ func (h *TrackHandler) GetUserLikedTracks(c *gin.Context) { } // GetUserRepostedTracks returns tracks reposted by the user (v0.10.3 F203). +// @Summary List tracks reposted by a user +// @Description Returns paginated tracks the user has reposted. Used for profile "Reposts" tab. +// @Tags User +// @Produce json +// @Param id path string true "User UUID" +// @Param limit query int false "Items per page (max 100)" default(20) +// @Param offset query int false "Offset for pagination" default(0) +// @Success 200 {object} handlers.APIResponse{data=object{tracks=[]models.Track,total=integer,limit=integer,offset=integer}} +// @Failure 400 {object} handlers.APIResponse "Validation" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /users/{id}/reposts [get] func (h *TrackHandler) GetUserRepostedTracks(c *gin.Context) { userIDStr := c.Param("id") if userIDStr == "" { @@ -243,6 +302,21 @@ func (h *TrackHandler) GetUserRepostedTracks(c *gin.Context) { } // CreateShare crée un nouveau lien de partage pour un track +// @Summary Create share link +// @Description Generate a tokenized share link for a track with given permission level and optional expiry. +// @Tags Track +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Param request body CreateShareRequest true "Share parameters" +// @Success 200 {object} handlers.APIResponse{data=object{share=object}} +// @Failure 400 {object} handlers.APIResponse "Validation" +// @Failure 401 {object} handlers.APIResponse "Unauthorized" +// @Failure 403 {object} handlers.APIResponse "Not owner" +// @Failure 404 {object} handlers.APIResponse "Track not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/share [post] func (h *TrackHandler) CreateShare(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { @@ -289,6 +363,17 @@ func (h *TrackHandler) CreateShare(c *gin.Context) { } // GetSharedTrack récupère un track via son token de partage +// @Summary Get track by share token +// @Description Public endpoint that resolves a share token and returns the track + share metadata. No auth required; the token IS the auth. +// @Tags Track +// @Produce json +// @Param token path string true "Opaque share token issued by CreateShare" +// @Success 200 {object} handlers.APIResponse{data=object{track=models.Track,share=object}} +// @Failure 400 {object} handlers.APIResponse "Missing token" +// @Failure 403 {object} handlers.APIResponse "Share link expired" +// @Failure 404 {object} handlers.APIResponse "Share or track not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/shared/{token} [get] func (h *TrackHandler) GetSharedTrack(c *gin.Context) { token := c.Param("token") if token == "" { @@ -332,6 +417,19 @@ func (h *TrackHandler) GetSharedTrack(c *gin.Context) { } // RevokeShare révoque un lien de partage +// @Summary Revoke share link +// @Description Permanently disable a share token. Only the share issuer (or admin) can revoke. +// @Tags Track +// @Produce json +// @Security BearerAuth +// @Param id path string true "Share UUID" +// @Success 200 {object} handlers.APIResponse{data=object{message=string}} +// @Failure 400 {object} handlers.APIResponse "Validation" +// @Failure 401 {object} handlers.APIResponse "Unauthorized" +// @Failure 403 {object} handlers.APIResponse "Not issuer" +// @Failure 404 {object} handlers.APIResponse "Share not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/share/{id} [delete] func (h *TrackHandler) RevokeShare(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { @@ -373,6 +471,18 @@ func (h *TrackHandler) RevokeShare(c *gin.Context) { } // RepostTrack adds a track repost to the user's profile (v0.10.3 F203). +// @Summary Repost a track +// @Description Add a track to the authenticated user's profile as a repost. Notifies the creator (F204) unless self-repost. +// @Tags Track +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Success 200 {object} handlers.APIResponse{data=object{message=string}} +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Failure 401 {object} handlers.APIResponse "Unauthorized" +// @Failure 404 {object} handlers.APIResponse "Track not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/repost [post] func (h *TrackHandler) RepostTrack(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { @@ -418,6 +528,17 @@ func (h *TrackHandler) RepostTrack(c *gin.Context) { } // UnrepostTrack removes a track repost (v0.10.3 F203). +// @Summary Remove track repost +// @Description Remove the authenticated user's repost of the track (idempotent). +// @Tags Track +// @Produce json +// @Security BearerAuth +// @Param id path string true "Track UUID" +// @Success 200 {object} handlers.APIResponse{data=object{message=string}} +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Failure 401 {object} handlers.APIResponse "Unauthorized" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/repost [delete] func (h *TrackHandler) UnrepostTrack(c *gin.Context) { userID, ok := h.getUserID(c) if !ok { @@ -451,6 +572,14 @@ func (h *TrackHandler) UnrepostTrack(c *gin.Context) { // GetRepostStatus returns whether the current user has reposted the track (v0.10.3 F203). // Works with OptionalAuth: if not authenticated, returns is_reposted: false. +// @Summary Get repost status +// @Description Returns whether the current user has reposted the track. Public (optional auth); unauthenticated callers get is_reposted=false. +// @Tags Track +// @Produce json +// @Param id path string true "Track UUID" +// @Success 200 {object} handlers.APIResponse{data=object{is_reposted=boolean}} +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Router /tracks/{id}/repost [get] func (h *TrackHandler) GetRepostStatus(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { diff --git a/veza-backend-api/internal/core/track/track_waveform_handler.go b/veza-backend-api/internal/core/track/track_waveform_handler.go index 0216be1a3..6cbbbc6a0 100644 --- a/veza-backend-api/internal/core/track/track_waveform_handler.go +++ b/veza-backend-api/internal/core/track/track_waveform_handler.go @@ -6,11 +6,25 @@ import ( "github.com/google/uuid" + // handlers imported so swaggo resolves handlers.APIResponse refs in + // doc comments; no direct call. + _ "veza-backend-api/internal/handlers" + "github.com/gin-gonic/gin" ) // GetWaveform returns the waveform JSON data for a track (S1-06) // GET /api/v1/tracks/:id/waveform +// @Summary Get track waveform +// @Description Returns a JSON peaks array used by the client to draw the audio waveform preview. 404 if waveform extraction is not complete yet. +// @Tags Track +// @Produce json +// @Param id path string true "Track UUID" +// @Success 200 {object} object "Waveform peaks JSON (tool-specific shape)" +// @Failure 400 {object} handlers.APIResponse "Invalid track id" +// @Failure 404 {object} handlers.APIResponse "Waveform not generated / track not found" +// @Failure 500 {object} handlers.APIResponse "Internal Error" +// @Router /tracks/{id}/waveform [get] func (h *TrackHandler) GetWaveform(c *gin.Context) { if h.waveformService == nil { h.respondWithError(c, http.StatusInternalServerError, "waveform service not available") diff --git a/veza-backend-api/openapi.yaml b/veza-backend-api/openapi.yaml index 37c39ee22..d4475668d 100644 --- a/veza-backend-api/openapi.yaml +++ b/veza-backend-api/openapi.yaml @@ -31,6 +31,19 @@ definitions: required: - upload_id type: object + internal_core_track.CreateShareRequest: + properties: + expires_at: + type: string + permissions: + enum: + - read + - write + - admin + type: string + required: + - permissions + type: object internal_core_track.InitiateChunkedUploadRequest: properties: filename: @@ -46,6 +59,27 @@ definitions: - total_chunks - total_size type: object + internal_core_track.RecordPlayRequest: + properties: + play_time: + minimum: 0 + type: integer + type: object + internal_core_track.StreamCallbackRequest: + properties: + error: + type: string + manifest_url: + type: string + status: + enum: + - completed + - failed + - processing + type: string + required: + - status + type: object internal_core_track.UpdateLyricsRequest: properties: content: @@ -759,6 +793,13 @@ definitions: value: type: string type: object + veza-backend-api_internal_handlers.APIResponse: + properties: + data: {} + error: {} + success: + type: boolean + type: object veza-backend-api_internal_models.Playlist: properties: collaborators: @@ -2084,6 +2125,46 @@ paths: summary: Get comment replies tags: - Comment + /internal/tracks/{id}/stream-ready: + post: + consumes: + - application/json + description: Internal endpoint called by the Rust stream server when HLS transcoding + completes or fails. Updates the track's stream_status and stream_manifest_url. + Requires internal API key (not user-facing). + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Callback payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.StreamCallbackRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + properties: + message: + type: string + type: object + "400": + description: Validation / invalid id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Stream server callback + tags: + - Track /playlists: get: consumes: @@ -2853,6 +2934,245 @@ paths: summary: Delete comment tags: - Comment + /tracks/{id}/download: + get: + description: Serve the original audio file. For S3-backed tracks returns a 302 + redirect to a signed URL (TTL 30min). For local-backed tracks streams the + file with Range support. Public tracks or share_token access; paid tracks + require a license. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Grants access without authentication for a limited time + in: query + name: share_token + type: string + produces: + - application/octet-stream + responses: + "200": + description: OK + schema: + type: file + "302": + description: Location header points to signed S3 URL (s3-backed tracks) + schema: + type: string + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: No permission / license required + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track or file not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Download a track + tags: + - Track + /tracks/{id}/history: + get: + description: Paginated audit log of modifications (metadata updates, version + changes) for a track. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - default: 50 + description: Items per page + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + history: + items: + type: object + type: array + limit: + type: integer + offset: + type: integer + total: + type: integer + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Get track history + tags: + - Track + /tracks/{id}/like: + delete: + description: Remove the authenticated user's like on the track (idempotent). + 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_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Unlike a track + tags: + - Track + post: + description: Record a like from the authenticated user. Creates a grouped notification + for the creator (F554). + 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_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Like a track + tags: + - Track + /tracks/{id}/likes: + get: + description: Returns whether the current user has liked the track. The total + like count is returned ONLY to the creator or an admin (privacy per ORIGIN_UI_UX_SYSTEM + §13). + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: count is omitted for non-owners + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + count: + type: integer + is_liked: + type: boolean + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Get track like status + tags: + - Track /tracks/{id}/lyrics: get: description: Returns the current lyrics for a track, or null if no lyrics exist. @@ -2949,6 +3269,276 @@ paths: summary: Create or update track lyrics tags: - Track + /tracks/{id}/play: + post: + consumes: + - application/json + description: Persist a playback event with optional play_time so the creator's + analytics dashboard tracks listening behaviour. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Playback metadata (optional) + in: body + name: request + schema: + $ref: '#/definitions/internal_core_track.RecordPlayRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + id: + type: string + message: + type: string + type: object + type: object + "400": + description: Invalid track id / body + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Record play event + tags: + - Track + /tracks/{id}/repost: + delete: + description: Remove the authenticated user's repost of the track (idempotent). + 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_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Remove track repost + tags: + - Track + get: + description: Returns whether the current user has reposted the track. Public + (optional auth); unauthenticated callers get is_reposted=false. + 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_handlers.APIResponse' + - properties: + data: + properties: + is_reposted: + type: boolean + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Get repost status + tags: + - Track + post: + description: Add a track to the authenticated user's profile as a repost. Notifies + the creator (F204) unless self-repost. + 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_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Repost a track + tags: + - Track + /tracks/{id}/share: + post: + consumes: + - application/json + description: Generate a tokenized share link for a track with given permission + level and optional expiry. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Share parameters + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_core_track.CreateShareRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + share: + type: object + type: object + type: object + "400": + description: Validation + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: Not owner + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Create share link + tags: + - Track + /tracks/{id}/stats: + get: + description: 'Aggregated track stats: views, likes, comments, play time, downloads, + average duration.' + 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_handlers.APIResponse' + - properties: + data: + properties: + stats: + type: object + type: object + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Get track statistics + tags: + - Track /tracks/{id}/status: get: consumes: @@ -2992,6 +3582,138 @@ paths: summary: Get Upload Status tags: - Track + /tracks/{id}/stream: + get: + description: Default playback path. S3-backed tracks return a 302 redirect to + a signed URL (TTL 15min). Local-backed tracks are streamed via http.ServeContent + with Range support. Always available, unlike /hls/* which is gated by HLSEnabled. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Grants access without authentication + in: query + name: share_token + type: string + produces: + - audio/* + responses: + "200": + description: OK + schema: + type: file + "302": + description: Location header points to signed S3 URL (s3-backed tracks) + schema: + type: string + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: No permission + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track or file not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Stream a track (raw audio + Range) + tags: + - Track + /tracks/{id}/versions/{versionId}/restore: + post: + description: Rollback a track to a previous version. Only the track owner can + restore. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + - description: Version UUID + in: path + name: versionId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Invalid id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: Not owner + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Track or version not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Restore track version + tags: + - Track + /tracks/{id}/waveform: + get: + description: Returns a JSON peaks array used by the client to draw the audio + waveform preview. 404 if waveform extraction is not complete yet. + parameters: + - description: Track UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Waveform peaks JSON (tool-specific shape) + schema: + type: object + "400": + description: Invalid track id + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Waveform not generated / track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Get track waveform + tags: + - Track /tracks/batch/delete: post: consumes: @@ -3291,6 +4013,47 @@ paths: summary: Get Upload Quota tags: - Track + /tracks/recommendations: + get: + description: Personalized tracks for D2 autoplay. If seed_track_id is given, + returns tracks similar to that seed. Otherwise, uses the caller's history + (chronological, no behavioural ranking — CLAUDE.md rule 7). + parameters: + - default: 20 + description: Max items (max 100) + in: query + name: limit + type: integer + - description: Start from this track's similarity neighbours + in: query + name: seed_track_id + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + tracks: + items: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: array + type: object + type: object + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + security: + - BearerAuth: [] + summary: Get track recommendations + tags: + - Track /tracks/resume/{uploadId}: get: consumes: @@ -3328,6 +4091,233 @@ paths: summary: Resume Upload tags: - Track + /tracks/search: + get: + description: Full-text + faceted search on tracks (genre, BPM, duration, tags, + musical key, dates). Sort-by and order configurable. + parameters: + - description: Full-text query (title/artist/album) + in: query + name: q + type: string + - description: Comma-separated tag list + in: query + name: tags + type: string + - default: OR + description: Tag combinator (OR / AND) + in: query + name: tag_mode + type: string + - description: Minimum duration (seconds) + in: query + name: min_duration + type: integer + - description: Maximum duration (seconds) + in: query + name: max_duration + type: integer + - description: Minimum BPM + in: query + name: min_bpm + type: integer + - description: Maximum BPM + in: query + name: max_bpm + type: integer + - description: Genre filter + in: query + name: genre + type: string + - description: Audio format filter + in: query + name: format + type: string + - description: Musical key filter + in: query + name: musical_key + type: string + - description: Created-after (RFC3339) + in: query + name: min_date + type: string + - description: Created-before (RFC3339) + in: query + name: max_date + type: string + - default: 1 + description: Page (1-based) + in: query + name: page + type: integer + - default: 20 + description: Items per page (max 100) + in: query + name: limit + type: integer + - default: created_at + description: Sort column + in: query + name: sort_by + type: string + - default: desc + description: asc / desc + in: query + name: sort_order + 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 + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + summary: Advanced track search + tags: + - Track + /tracks/share/{id}: + delete: + description: Permanently disable a share token. Only the share issuer (or admin) + can revoke. + parameters: + - description: Share UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + message: + type: string + type: object + type: object + "400": + description: Validation + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: Not issuer + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Share not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: Revoke share link + tags: + - Track + /tracks/shared/{token}: + get: + description: Public endpoint that resolves a share token and returns the track + + share metadata. No auth required; the token IS the auth. + parameters: + - description: Opaque share token issued by CreateShare + in: path + name: token + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + share: + type: object + track: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: object + type: object + "400": + description: Missing token + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "403": + description: Share link expired + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "404": + description: Share or track not found + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: Get track by share token + tags: + - Track + /tracks/suggested-tags: + get: + description: Returns a static tag suggestion list for a genre — useful for upload + autocomplete and filter chips. + parameters: + - default: default + description: Genre slug (pop, rock, electronic, hip-hop, jazz, classical, + ambient, default) + in: query + name: genre + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_response.APIResponse' + - properties: + data: + properties: + tags: + items: + type: string + type: array + type: object + type: object + summary: Get suggested tags + tags: + - Track /users: get: consumes: @@ -3555,6 +4545,120 @@ paths: summary: Get Profile Completion tags: - User + /users/{id}/likes: + get: + description: Returns paginated tracks the given user has liked. Used for profile + "Likes" tab. + parameters: + - description: User UUID + in: path + name: id + required: true + type: string + - default: 20 + description: Items per page (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset for pagination + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + limit: + type: integer + offset: + type: integer + total: + type: integer + tracks: + items: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: array + type: object + type: object + "400": + description: Validation + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + security: + - BearerAuth: [] + summary: List tracks liked by a user + tags: + - User + /users/{id}/reposts: + get: + description: Returns paginated tracks the user has reposted. Used for profile + "Reposts" tab. + parameters: + - description: User UUID + in: path + name: id + required: true + type: string + - default: 20 + description: Items per page (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset for pagination + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + - properties: + data: + properties: + limit: + type: integer + offset: + type: integer + total: + type: integer + tracks: + items: + $ref: '#/definitions/veza-backend-api_internal_models.Track' + type: array + type: object + type: object + "400": + description: Validation + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + "500": + description: Internal Error + schema: + $ref: '#/definitions/veza-backend-api_internal_handlers.APIResponse' + summary: List tracks reposted by a user + tags: + - User /users/by-username/{username}: get: consumes: