diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index b600fd9b..ca6eea45 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -982,6 +982,7 @@ paths: schema: type: string requestBody: + x-codegen-request-body-name: metadata required: true content: application/json: @@ -1221,6 +1222,7 @@ paths: schema: type: string requestBody: + x-codegen-request-body-name: metadata required: true content: application/json: @@ -1697,6 +1699,7 @@ paths: schema: type: string requestBody: + x-codegen-request-body-name: metadata required: true content: application/json: @@ -2521,6 +2524,7 @@ paths: schema: type: string requestBody: + x-codegen-request-body-name: metadata required: true content: application/json: @@ -3176,6 +3180,12 @@ paths: required: true schema: type: string + - name: user_id + in: query + description: User ID + required: true + schema: + type: string responses: "200": description: Track share recorded successfully @@ -3411,6 +3421,7 @@ paths: schema: type: string requestBody: + x-codegen-request-body-name: metadata required: true content: application/json: @@ -3828,6 +3839,7 @@ paths: schema: type: string requestBody: + x-codegen-request-body-name: metadata required: true content: application/json: @@ -6731,6 +6743,7 @@ components: type: string release_date: type: string + format: date isrc: type: string remix_of: @@ -6897,6 +6910,15 @@ components: type: array items: $ref: '#/components/schemas/track_element' + remix_parent_write: + type: object + required: + - tracks + properties: + tracks: + type: array + items: + $ref: '#/components/schemas/track_element_write' track_element: required: - parent_track_id @@ -6904,6 +6926,13 @@ components: properties: parent_track_id: type: string + track_element_write: + required: + - parent_track_id + type: object + properties: + parent_track_id: + type: integer album_backlink: required: - permalink @@ -7092,6 +7121,7 @@ components: type: boolean release_date: type: string + format: date ddex_release_ids: type: object properties: {} @@ -7806,7 +7836,6 @@ components: - $ref: '#/components/schemas/tip_gate' - $ref: '#/components/schemas/follow_gate' - $ref: '#/components/schemas/extended_purchase_gate' - - $ref: '#/components/schemas/nft_gate' - $ref: '#/components/schemas/token_gate' tip_gate: required: @@ -7886,16 +7915,6 @@ components: type: string amount: type: integer - nft_gate: - required: - - nft_collection - type: object - properties: - nft_collection: - type: object - description: Must hold an NFT of the given collection to unlock - allOf: - - $ref: '#/components/schemas/nft_collection' media_link: type: object properties: @@ -7906,45 +7925,11 @@ components: items: type: string access_gate: - type: object - properties: - usdc_purchase: - $ref: '#/components/schemas/extended_purchase_gate' - follow_user_id: - type: integer - tip_user_id: - type: integer - nft_collection: - type: object - token_gate: - $ref: '#/components/schemas/extended_token_gate' - nft_collection: - required: - - address - - chain - - name - type: object - properties: - chain: - type: string - example: eth - enum: - - eth - - sol - standard: - type: string - example: ERC721 - enum: - - ERC721 - - ERC1155 - address: - type: string - name: - type: string - imageUrl: - type: string - externalLink: - type: string + oneOf: + - $ref: '#/components/schemas/tip_gate' + - $ref: '#/components/schemas/follow_gate' + - $ref: '#/components/schemas/extended_purchase_gate' + - $ref: '#/components/schemas/token_gate' track_response: type: object properties: @@ -8113,6 +8098,31 @@ components: type: integer orig_filename: type: string + stem_category: + type: string + enum: + - INSTRUMENTAL + - LEAD_VOCALS + - MELODIC_LEAD + - PAD + - SNARE + - KICK + - HIHAT + - PERCUSSION + - SAMPLE + - BACKING_VOX + - BASS + - OTHER + stem_parent: + required: + - category + - parent_track_id + type: object + properties: + category: + $ref: '#/components/schemas/stem_category' + parent_track_id: + type: integer undisbursed_challenges: type: object properties: @@ -9760,8 +9770,9 @@ components: - track_cid properties: track_id: - type: string + type: integer description: Optional track ID (will be generated if not provided) + minimum: 1 title: type: string description: Track title @@ -9774,6 +9785,12 @@ components: maxLength: 1000 mood: $ref: '#/components/schemas/mood' + bpm: + type: number + description: Beats per minute (tempo) + musical_key: + type: string + description: Musical key of the track tags: type: string description: Comma-separated tags @@ -9788,13 +9805,23 @@ components: description: International Standard Musical Work Code release_date: type: string + format: date description: Release date track_cid: type: string description: IPFS CID for the track audio file (required) + orig_file_cid: + type: string + description: IPFS CID for the original track file + orig_filename: + type: string + description: Original filename of the track cover_art_cid: type: string description: IPFS CID for cover art + cover_art_sizes: + type: string + description: Cover art sizes metadata preview_cid: type: string description: IPFS CID for the track preview @@ -9814,6 +9841,19 @@ components: is_unlisted: type: boolean description: Whether the track is unlisted + stream_conditions: + $ref: '#/components/schemas/access_gate' + download_conditions: + $ref: '#/components/schemas/access_gate' + field_visibility: + $ref: '#/components/schemas/field_visibility' + placement_hosts: + type: string + description: Placement hosts for the track + stem_of: + $ref: '#/components/schemas/stem_parent' + remix_of: + $ref: '#/components/schemas/remix_parent_write' ddex_app: type: string description: DDEX application identifier @@ -9827,42 +9867,113 @@ components: title: type: string description: Track title + genre: + $ref: '#/components/schemas/genre' description: type: string + nullable: true description: Track description - maxLength: 5000 - genre: - $ref: '#/components/schemas/genre' + maxLength: 1000 mood: - $ref: '#/components/schemas/mood' + nullable: true + allOf: + - $ref: '#/components/schemas/mood' + bpm: + type: number + nullable: true + description: Beats per minute (tempo) + musical_key: + type: string + nullable: true + description: Musical key of the track tags: type: string + nullable: true description: Comma-separated tags - release_date: + license: type: string - format: date-time - description: Track release date + nullable: true + description: License type isrc: type: string + nullable: true description: International Standard Recording Code - is_downloadable: + iswc: + type: string + nullable: true + description: International Standard Musical Work Code + release_date: + type: string + format: date + description: Release date + track_cid: + type: string + description: IPFS CID for the track audio file + orig_file_cid: + type: string + description: IPFS CID for the original track file + orig_filename: + type: string + description: Original filename of the track + cover_art_cid: + type: string + description: IPFS CID for cover art + cover_art_sizes: + type: string + description: Cover art sizes metadata + preview_cid: + type: string + description: IPFS CID for the track preview + preview_start_seconds: + type: number + format: float + description: Preview start time in seconds + minimum: 0 + duration: + type: number + format: float + description: Track duration in seconds + minimum: 0 + downloadable: type: boolean description: Whether the track is downloadable - is_original_available: + is_unlisted: type: boolean - description: Whether the original file is available - artwork_url: + description: Whether the track is unlisted + stream_conditions: + nullable: true + allOf: + - $ref: '#/components/schemas/access_gate' + download_conditions: + nullable: true + allOf: + - $ref: '#/components/schemas/access_gate' + field_visibility: + $ref: '#/components/schemas/field_visibility' + placement_hosts: type: string - description: URL for track artwork - format: uri + description: Placement hosts for the track + stem_of: + $ref: '#/components/schemas/stem_parent' + remix_of: + $ref: '#/components/schemas/remix_parent_write' + ddex_app: + type: string + nullable: true + description: DDEX application identifier + parental_warning_type: + type: string + nullable: true + description: Parental warning type create_playlist_request_body: type: object required: - playlist_name properties: playlist_id: - type: string + type: integer description: Optional playlist ID (will be generated if not provided) + minimum: 1 playlist_name: type: string description: Playlist or album name @@ -9925,6 +10036,20 @@ components: is_private: type: boolean description: Whether the playlist is private + genre: + $ref: '#/components/schemas/genre' + mood: + $ref: '#/components/schemas/mood' + tags: + type: string + description: Comma-separated tags + license: + type: string + description: License type + release_date: + type: string + format: date + description: Release date playlist_contents: type: array description: Array of track IDs to include in the playlist @@ -9943,8 +10068,9 @@ components: - wallet properties: user_id: - type: string + type: integer description: Optional user ID (will be generated if not provided) + minimum: 1 handle: type: string description: User handle (unique username) @@ -10211,6 +10337,21 @@ components: description: Type of entity that can be commented on enum: - Track + field_visibility: + type: object + properties: + mood: + type: boolean + tags: + type: boolean + genre: + type: boolean + share: + type: boolean + play_count: + type: boolean + remixes: + type: boolean genre: type: string description: Music genre diff --git a/api/v1_comments.go b/api/v1_comments.go index 452e9a02..ff70d761 100644 --- a/api/v1_comments.go +++ b/api/v1_comments.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "math" "strconv" "time" @@ -139,8 +140,14 @@ func (app *ApiServer) postV1Comment(c *fiber.Ctx) error { if req.CommentId != nil { commentID = *req.CommentId } else { - // Generate random comment ID if not provided - commentID = int(nonce % 1000000) + // Generate unclaimed comment ID if not provided + generatedID, err := app.generateUnclaimedId(c.Context(), "comments", "comment_id", 4_000_000, math.MaxInt32) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to generate comment ID: " + err.Error(), + }) + } + commentID = generatedID } // Build metadata diff --git a/api/v1_playlist.go b/api/v1_playlist.go index fa3a3fa2..bc57a4db 100644 --- a/api/v1_playlist.go +++ b/api/v1_playlist.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "math" "strconv" "time" @@ -21,7 +22,7 @@ type PlaylistTrackInfo struct { } type CreatePlaylistRequest struct { - PlaylistId *string `json:"playlist_id,omitempty"` + PlaylistId *int `json:"playlist_id,omitempty" validate:"omitempty,min=1"` PlaylistName string `json:"playlist_name" validate:"required,min=1"` Description *string `json:"description,omitempty" validate:"omitempty,max=1000"` IsPrivate *bool `json:"is_private,omitempty"` @@ -107,16 +108,16 @@ func (app *ApiServer) postV1Playlists(c *fiber.Ctx) error { // Determine playlist ID var playlistID int if req.PlaylistId != nil { - decodedID, err := trashid.DecodeHashId(*req.PlaylistId) + playlistID = *req.PlaylistId + } else { + // Generate unclaimed playlist ID if not provided + generatedID, err := app.generateUnclaimedId(c.Context(), "tracks", "track_id", 400_000, math.MaxInt32) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid playlist_id: " + err.Error(), + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to generate playlist ID: " + err.Error(), }) } - playlistID = decodedID - } else { - // Generate playlist ID from timestamp if not provided - playlistID = int(time.Now().UnixNano() % 1000000000) + playlistID = generatedID } // Convert struct to map for metadata diff --git a/api/v1_track.go b/api/v1_track.go index 7978af94..b57102d3 100644 --- a/api/v1_track.go +++ b/api/v1_track.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "math" "strconv" "time" @@ -17,7 +18,7 @@ import ( // Nested type definitions for track metadata type RemixParent struct { - ParentTrackId string `json:"parent_track_id" validate:"required"` + ParentTrackId int `json:"parent_track_id" validate:"required,min=1"` } type RemixOf struct { @@ -25,8 +26,8 @@ type RemixOf struct { } type StemOf struct { - Category string `json:"category" validate:"required,oneof=BASS DRUMS MELODY VOCALS OTHER"` - ParentTrackId string `json:"parent_track_id" validate:"required"` + Category string `json:"category" validate:"required,oneof=INSTRUMENTAL LEAD_VOCALS MELODIC_LEAD PAD SNARE KICK HIHAT PERCUSSION SAMPLE BACKING_VOX BASS OTHER"` + ParentTrackId int `json:"parent_track_id" validate:"required,min=1"` } type FieldVisibility struct { @@ -113,7 +114,7 @@ type DDEXRightsController struct { } type CreateTrackRequest struct { - TrackId *string `json:"track_id,omitempty"` + TrackId *int `json:"track_id,omitempty" validate:"omitempty,min=1"` Title string `json:"title" validate:"required,min=1"` Genre string `json:"genre" validate:"required,oneof='Electronic' 'Rock' 'Metal' 'Alternative' 'Hip-Hop/Rap' 'Experimental' 'Punk' 'Folk' 'Pop' 'Ambient' 'Soundtrack' 'World' 'Jazz' 'Acoustic' 'Funk' 'R&B/Soul' 'Devotional' 'Classical' 'Reggae' 'Podcasts' 'Country' 'Spoken Word' 'Comedy' 'Blues' 'Kids' 'Audiobooks' 'Latin' 'Lo-Fi' 'Hyperpop' 'Dancehall' 'Techno' 'Trap' 'House' 'Tech House' 'Deep House' 'Disco' 'Electro' 'Jungle' 'Progressive House' 'Hardstyle' 'Glitch Hop' 'Trance' 'Future Bass' 'Future House' 'Tropical House' 'Downtempo' 'Drum & Bass' 'Dubstep' 'Jersey Club' 'Vaporwave' 'Moombahton'"` Description *string `json:"description,omitempty" validate:"omitempty,max=1000"` @@ -234,16 +235,16 @@ func (app *ApiServer) postV1Tracks(c *fiber.Ctx) error { // Determine track ID var trackID int if req.TrackId != nil { - decodedID, err := trashid.DecodeHashId(*req.TrackId) + trackID = *req.TrackId + } else { + // Generate unclaimed track ID if not provided + generatedID, err := app.generateUnclaimedId(c.Context(), "tracks", "track_id", 2_000_000, math.MaxInt32) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid track_id: " + err.Error(), + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to generate track ID: " + err.Error(), }) } - trackID = decodedID - } else { - // Generate track ID from timestamp if not provided - trackID = int(time.Now().UnixNano() % 1000000000) + trackID = generatedID } // Convert struct to map for metadata diff --git a/api/v1_unclaimed_id.go b/api/v1_unclaimed_id.go index 7911e9b6..fe77126b 100644 --- a/api/v1_unclaimed_id.go +++ b/api/v1_unclaimed_id.go @@ -1,6 +1,7 @@ package api import ( + "context" "fmt" "math" "math/rand" @@ -10,23 +11,26 @@ import ( "github.com/jackc/pgx/v5" ) -func (app *ApiServer) v1UnclaimedId(c *fiber.Ctx, tableName, idField string, lowerBound, upperBound int) error { - +// generateUnclaimedId finds an unclaimed integer ID in the given range for the specified table +func (app *ApiServer) generateUnclaimedId(ctx context.Context, tableName, idField string, lowerBound, upperBound int) (int, error) { sql := fmt.Sprintf(`select true from %s where %s = $1`, tableName, idField) - freeId := 0 for i := 0; i < 50; i++ { randomId := lowerBound + rand.Intn(upperBound-lowerBound) isTaken := false - err := app.pool.QueryRow(c.Context(), sql, randomId).Scan(&isTaken) + err := app.pool.QueryRow(ctx, sql, randomId).Scan(&isTaken) if err == pgx.ErrNoRows { - freeId = randomId - break + return randomId, nil } } - if freeId == 0 { - return fiber.NewError(500, "unable to find unclaimed id") + return 0, fmt.Errorf("unable to find unclaimed id after 50 attempts") +} + +func (app *ApiServer) v1UnclaimedId(c *fiber.Ctx, tableName, idField string, lowerBound, upperBound int) error { + freeId, err := app.generateUnclaimedId(c.Context(), tableName, idField, lowerBound, upperBound) + if err != nil { + return fiber.NewError(500, err.Error()) } id, _ := trashid.EncodeHashId(freeId) diff --git a/api/v1_users.go b/api/v1_users.go index 59a0afba..26ce93eb 100644 --- a/api/v1_users.go +++ b/api/v1_users.go @@ -27,7 +27,7 @@ type PlaylistLibrary struct { } type CreateUserRequest struct { - UserId *string `json:"user_id,omitempty"` + UserId *int `json:"user_id,omitempty" validate:"omitempty,min=1"` Handle string `json:"handle" validate:"required,min=1"` Wallet string `json:"wallet" validate:"required"` Name *string `json:"name,omitempty" validate:"omitempty,min=1"` @@ -106,16 +106,16 @@ func (app *ApiServer) postV1Users(c *fiber.Ctx) error { // Determine user ID var userID int if req.UserId != nil { - decodedID, err := trashid.DecodeHashId(*req.UserId) + userID = *req.UserId + } else { + // Generate unclaimed user ID if not provided + generatedID, err := app.generateUnclaimedId(c.Context(), "users", "user_id", 3_000_000, 999_999_999) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid user_id: " + err.Error(), + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to generate user ID: " + err.Error(), }) } - userID = decodedID - } else { - // Generate user ID from timestamp if not provided - userID = int(time.Now().UnixNano() % 1000000000) + userID = generatedID } // Convert struct to map for metadata