From 153e735e0aa900e6005822bdac0e9187383210c2 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Fri, 29 May 2026 10:00:09 -0700 Subject: [PATCH 1/2] feat(genres): allow custom track genres across the stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - discovery indexer: drop genre allowlist enforcement at write time; cap genre at 100 chars; allowlist retained as canonical reference for read-side trending/metrics aggregations - sdk: relax track + album + playlist upload schemas to z.string().min(1).max(100); regenerate from updated swagger (generated Genre enum dropped); manual Genre enum re-exported from index with PascalCase keys to preserve @audius/sdk consumer compatibility; new GenreString = Genre | string helper type - web: SelectGenreField rewritten to TextField + native so users can pick a known genre via autocomplete or type a custom one Read-side genre filters (trending, get_genre_metrics) still scope to the canonical set — tracks tagged with custom genres won't appear in those endpoints. Mobile genre picker is untouched and will be addressed in a follow-up PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tasks/entity_manager/entities/track.py | 6 +- .../src/tasks/entity_manager/utils.py | 1 + .../src/utils/hardcoded_data.py | 4 + .../sdk/src/sdk/api/albums/AlbumsApi.test.ts | 3 +- packages/sdk/src/sdk/api/albums/types.ts | 4 +- .../default/.openapi-generator/FILES | 1 - .../models/CreatePlaylistRequestBody.ts | 23 ++-- .../default/models/CreateTrackRequestBody.ts | 23 ++-- .../sdk/api/generated/default/models/Genre.ts | 87 ------------- .../models/UpdatePlaylistRequestBody.ts | 23 ++-- .../default/models/UpdateTrackRequestBody.ts | 23 ++-- .../sdk/api/generated/default/models/index.ts | 1 - .../sdk/api/playlists/PlaylistsApi.test.ts | 3 +- packages/sdk/src/sdk/api/playlists/types.ts | 3 +- .../sdk/src/sdk/api/tracks/TracksApi.test.ts | 4 +- packages/sdk/src/sdk/api/tracks/types.ts | 3 +- packages/sdk/src/sdk/index.ts | 2 + packages/sdk/src/sdk/types/Genre.ts | 119 ++++++++++-------- .../edit/fields/SelectGenreField.tsx | 38 +++--- 19 files changed, 163 insertions(+), 208 deletions(-) delete mode 100644 packages/sdk/src/sdk/api/generated/default/models/Genre.ts diff --git a/packages/discovery-provider/src/tasks/entity_manager/entities/track.py b/packages/discovery-provider/src/tasks/entity_manager/entities/track.py index 4f085354916..900863076ab 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/entities/track.py +++ b/packages/discovery-provider/src/tasks/entity_manager/entities/track.py @@ -28,6 +28,7 @@ from src.models.users.user import User from src.tasks.entity_manager.utils import ( CHARACTER_LIMIT_DESCRIPTION, + CHARACTER_LIMIT_GENRE, TRACK_ID_OFFSET, Action, EntityType, @@ -45,7 +46,6 @@ ) from src.tasks.task_helpers import generate_slug_and_collision_id from src.utils import helpers -from src.utils.hardcoded_data import genre_allowlist from src.utils.structured_logger import StructuredLogger logger = StructuredLogger(__name__) @@ -534,9 +534,9 @@ def validate_track_tx(params: ManageEntityParameters): ) track_bio = params.metadata.get("description") track_genre = params.metadata.get("genre") - if track_genre is not None and track_genre not in genre_allowlist: + if track_genre is not None and len(track_genre) > CHARACTER_LIMIT_GENRE: raise IndexingValidationError( - f"Track {track_id} attempted to be placed in genre '{track_genre}' which is not in the allow list" + f"Track {track_id} genre exceeds character limit {CHARACTER_LIMIT_GENRE}" ) if track_bio is not None and len(track_bio) > CHARACTER_LIMIT_DESCRIPTION: raise IndexingValidationError( diff --git a/packages/discovery-provider/src/tasks/entity_manager/utils.py b/packages/discovery-provider/src/tasks/entity_manager/utils.py index 5e34039912a..b27316f0227 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/utils.py +++ b/packages/discovery-provider/src/tasks/entity_manager/utils.py @@ -67,6 +67,7 @@ # limits CHARACTER_LIMIT_USER_BIO = 256 CHARACTER_LIMIT_DESCRIPTION = 2500 +CHARACTER_LIMIT_GENRE = 100 PLAYLIST_TRACK_LIMIT = 5000 COMMENT_BODY_LIMIT = 400 diff --git a/packages/discovery-provider/src/utils/hardcoded_data.py b/packages/discovery-provider/src/utils/hardcoded_data.py index 420c4d252a9..2bc28ae90a8 100644 --- a/packages/discovery-provider/src/utils/hardcoded_data.py +++ b/packages/discovery-provider/src/utils/hardcoded_data.py @@ -96,6 +96,10 @@ def has_badwords(s: str) -> bool: return any(badword in f for badword in handle_badwords_lower) +# Known/canonical genres. No longer enforced as an allowlist at write time — +# track genres can be arbitrary strings (capped at CHARACTER_LIMIT_GENRE). +# Still used by trending (index_trending.py) and genre metrics +# (get_genre_metrics.py) to scope read-side aggregations to canonical values. genre_allowlist = { "Acoustic", "Alternative", diff --git a/packages/sdk/src/sdk/api/albums/AlbumsApi.test.ts b/packages/sdk/src/sdk/api/albums/AlbumsApi.test.ts index 3d9d10120eb..af1f8e2eaab 100644 --- a/packages/sdk/src/sdk/api/albums/AlbumsApi.test.ts +++ b/packages/sdk/src/sdk/api/albums/AlbumsApi.test.ts @@ -20,7 +20,8 @@ import { import { SolanaClient } from '../../services/Solana/programs/SolanaClient' import { Storage } from '../../services/Storage' import { StorageNodeSelector } from '../../services/StorageNodeSelector' -import { Configuration, Genre, Mood } from '../generated/default' +import { Configuration, Mood } from '../generated/default' +import { Genre } from '../../types/Genre' import { PlaylistsApi as GeneratedPlaylistsApi } from '../generated/default/apis/PlaylistsApi' import type { PlaylistResponse } from '../generated/default/models/PlaylistResponse' import { TrackUploadHelper } from '../tracks/TrackUploadHelper' diff --git a/packages/sdk/src/sdk/api/albums/types.ts b/packages/sdk/src/sdk/api/albums/types.ts index a04f39b3760..cbbd5828ac3 100644 --- a/packages/sdk/src/sdk/api/albums/types.ts +++ b/packages/sdk/src/sdk/api/albums/types.ts @@ -14,7 +14,7 @@ import { PublicKeySchema } from '../../services/Solana' import { DDEXResourceContributor, DDEXCopyright } from '../../types/DDEX' import { AudioFile, ImageFile } from '../../types/File' import { HashId } from '../../types/HashId' -import { Mood, Genre } from '../generated/default' +import { Mood } from '../generated/default' import type { CreatePlaylistRequestBody, UpdatePlaylistRequestBody @@ -159,7 +159,7 @@ export const CreateAlbumSchema = z export type EntityManagerCreateAlbumRequest = z.input export const UploadAlbumMetadataSchema = CreateAlbumMetadataSchema.extend({ - genre: z.enum(Object.values(Genre) as [Genre, ...Genre[]]), + genre: z.string().min(1).max(100), mood: z.optional(z.enum(Object.values(Mood) as [Mood, ...Mood[]])), tags: z.optional(z.string()) }) diff --git a/packages/sdk/src/sdk/api/generated/default/.openapi-generator/FILES b/packages/sdk/src/sdk/api/generated/default/.openapi-generator/FILES index 8f0170350dc..a2a5b73fb01 100644 --- a/packages/sdk/src/sdk/api/generated/default/.openapi-generator/FILES +++ b/packages/sdk/src/sdk/api/generated/default/.openapi-generator/FILES @@ -191,7 +191,6 @@ models/FollowNotificationAction.ts models/FollowNotificationActionData.ts models/FollowersResponse.ts models/FollowingResponse.ts -models/Genre.ts models/GetChallenges.ts models/GetSupportedUsers.ts models/GetSupporter.ts diff --git a/packages/sdk/src/sdk/api/generated/default/models/CreatePlaylistRequestBody.ts b/packages/sdk/src/sdk/api/generated/default/models/CreatePlaylistRequestBody.ts index 9a8c86bd67f..6571203e54c 100644 --- a/packages/sdk/src/sdk/api/generated/default/models/CreatePlaylistRequestBody.ts +++ b/packages/sdk/src/sdk/api/generated/default/models/CreatePlaylistRequestBody.ts @@ -37,12 +37,6 @@ import { DdexResourceContributorFromJSONTyped, DdexResourceContributorToJSON, } from './DdexResourceContributor'; -import type { Genre } from './Genre'; -import { - GenreFromJSON, - GenreFromJSONTyped, - GenreToJSON, -} from './Genre'; import type { Mood } from './Mood'; import { MoodFromJSON, @@ -93,11 +87,20 @@ export interface CreatePlaylistRequestBody { */ isAlbum?: boolean; /** + * Music genre. Any string up to 100 characters is accepted. Known/canonical + * values (shown as autocomplete suggestions in clients): 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. * - * @type {Genre} + * @type {string} * @memberof CreatePlaylistRequestBody */ - genre?: Genre; + genre?: string; /** * * @type {Mood} @@ -227,7 +230,7 @@ export function CreatePlaylistRequestBodyFromJSONTyped(json: any, ignoreDiscrimi 'description': !exists(json, 'description') ? undefined : json['description'], 'isPrivate': !exists(json, 'is_private') ? undefined : json['is_private'], 'isAlbum': !exists(json, 'is_album') ? undefined : json['is_album'], - 'genre': !exists(json, 'genre') ? undefined : GenreFromJSON(json['genre']), + 'genre': !exists(json, 'genre') ? undefined : json['genre'], 'mood': !exists(json, 'mood') ? undefined : MoodFromJSON(json['mood']), 'tags': !exists(json, 'tags') ? undefined : json['tags'], 'license': !exists(json, 'license') ? undefined : json['license'], @@ -262,7 +265,7 @@ export function CreatePlaylistRequestBodyToJSON(value?: CreatePlaylistRequestBod 'description': value.description, 'is_private': value.isPrivate, 'is_album': value.isAlbum, - 'genre': GenreToJSON(value.genre), + 'genre': value.genre, 'mood': MoodToJSON(value.mood), 'tags': value.tags, 'license': value.license, diff --git a/packages/sdk/src/sdk/api/generated/default/models/CreateTrackRequestBody.ts b/packages/sdk/src/sdk/api/generated/default/models/CreateTrackRequestBody.ts index 6e527894beb..93592ed6176 100644 --- a/packages/sdk/src/sdk/api/generated/default/models/CreateTrackRequestBody.ts +++ b/packages/sdk/src/sdk/api/generated/default/models/CreateTrackRequestBody.ts @@ -49,12 +49,6 @@ import { FieldVisibilityFromJSONTyped, FieldVisibilityToJSON, } from './FieldVisibility'; -import type { Genre } from './Genre'; -import { - GenreFromJSON, - GenreFromJSONTyped, - GenreToJSON, -} from './Genre'; import type { Mood } from './Mood'; import { MoodFromJSON, @@ -93,11 +87,20 @@ export interface CreateTrackRequestBody { */ title: string; /** + * Music genre. Any string up to 100 characters is accepted. Known/canonical + * values (shown as autocomplete suggestions in clients): 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. * - * @type {Genre} + * @type {string} * @memberof CreateTrackRequestBody */ - genre: Genre; + genre: string; /** * Track description * @type {string} @@ -370,7 +373,7 @@ export function CreateTrackRequestBodyFromJSONTyped(json: any, ignoreDiscriminat 'trackId': !exists(json, 'track_id') ? undefined : json['track_id'], 'title': json['title'], - 'genre': GenreFromJSON(json['genre']), + 'genre': json['genre'], 'description': !exists(json, 'description') ? undefined : json['description'], 'mood': !exists(json, 'mood') ? undefined : MoodFromJSON(json['mood']), 'bpm': !exists(json, 'bpm') ? undefined : json['bpm'], @@ -426,7 +429,7 @@ export function CreateTrackRequestBodyToJSON(value?: CreateTrackRequestBody | nu 'track_id': value.trackId, 'title': value.title, - 'genre': GenreToJSON(value.genre), + 'genre': value.genre, 'description': value.description, 'mood': MoodToJSON(value.mood), 'bpm': value.bpm, diff --git a/packages/sdk/src/sdk/api/generated/default/models/Genre.ts b/packages/sdk/src/sdk/api/generated/default/models/Genre.ts deleted file mode 100644 index 78adbf7bac8..00000000000 --- a/packages/sdk/src/sdk/api/generated/default/models/Genre.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * Audius API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -/** - * Music genre - * @export - */ -export const Genre = { - Electronic: 'Electronic', - Rock: 'Rock', - Metal: 'Metal', - Alternative: 'Alternative', - HipHopRap: 'Hip-Hop/Rap', - Experimental: 'Experimental', - Punk: 'Punk', - Folk: 'Folk', - Pop: 'Pop', - Ambient: 'Ambient', - Soundtrack: 'Soundtrack', - World: 'World', - Jazz: 'Jazz', - Acoustic: 'Acoustic', - Funk: 'Funk', - RbSoul: 'R&B/Soul', - Devotional: 'Devotional', - Classical: 'Classical', - Reggae: 'Reggae', - Podcasts: 'Podcasts', - Country: 'Country', - SpokenWord: 'Spoken Word', - Comedy: 'Comedy', - Blues: 'Blues', - Kids: 'Kids', - Audiobooks: 'Audiobooks', - Latin: 'Latin', - LoFi: 'Lo-Fi', - Hyperpop: 'Hyperpop', - Dancehall: 'Dancehall', - Techno: 'Techno', - Trap: 'Trap', - House: 'House', - TechHouse: 'Tech House', - DeepHouse: 'Deep House', - Disco: 'Disco', - Electro: 'Electro', - Jungle: 'Jungle', - ProgressiveHouse: 'Progressive House', - Hardstyle: 'Hardstyle', - GlitchHop: 'Glitch Hop', - Trance: 'Trance', - FutureBass: 'Future Bass', - FutureHouse: 'Future House', - TropicalHouse: 'Tropical House', - Downtempo: 'Downtempo', - DrumBass: 'Drum & Bass', - Dubstep: 'Dubstep', - JerseyClub: 'Jersey Club', - Vaporwave: 'Vaporwave', - Moombahton: 'Moombahton' -} as const; -export type Genre = typeof Genre[keyof typeof Genre]; - - -export function GenreFromJSON(json: any): Genre { - return GenreFromJSONTyped(json, false); -} - -export function GenreFromJSONTyped(json: any, ignoreDiscriminator: boolean): Genre { - return json as Genre; -} - -export function GenreToJSON(value?: Genre | null): any { - return value as any; -} - diff --git a/packages/sdk/src/sdk/api/generated/default/models/UpdatePlaylistRequestBody.ts b/packages/sdk/src/sdk/api/generated/default/models/UpdatePlaylistRequestBody.ts index a6093ce21b5..86d1651cd7e 100644 --- a/packages/sdk/src/sdk/api/generated/default/models/UpdatePlaylistRequestBody.ts +++ b/packages/sdk/src/sdk/api/generated/default/models/UpdatePlaylistRequestBody.ts @@ -37,12 +37,6 @@ import { DdexResourceContributorFromJSONTyped, DdexResourceContributorToJSON, } from './DdexResourceContributor'; -import type { Genre } from './Genre'; -import { - GenreFromJSON, - GenreFromJSONTyped, - GenreToJSON, -} from './Genre'; import type { Mood } from './Mood'; import { MoodFromJSON, @@ -87,11 +81,20 @@ export interface UpdatePlaylistRequestBody { */ isAlbum?: boolean; /** + * Music genre. Any string up to 100 characters is accepted. Known/canonical + * values (shown as autocomplete suggestions in clients): 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. * - * @type {Genre} + * @type {string} * @memberof UpdatePlaylistRequestBody */ - genre?: Genre; + genre?: string; /** * * @type {Mood} @@ -219,7 +222,7 @@ export function UpdatePlaylistRequestBodyFromJSONTyped(json: any, ignoreDiscrimi 'description': !exists(json, 'description') ? undefined : json['description'], 'isPrivate': !exists(json, 'is_private') ? undefined : json['is_private'], 'isAlbum': !exists(json, 'is_album') ? undefined : json['is_album'], - 'genre': !exists(json, 'genre') ? undefined : GenreFromJSON(json['genre']), + 'genre': !exists(json, 'genre') ? undefined : json['genre'], 'mood': !exists(json, 'mood') ? undefined : MoodFromJSON(json['mood']), 'tags': !exists(json, 'tags') ? undefined : json['tags'], 'license': !exists(json, 'license') ? undefined : json['license'], @@ -253,7 +256,7 @@ export function UpdatePlaylistRequestBodyToJSON(value?: UpdatePlaylistRequestBod 'description': value.description, 'is_private': value.isPrivate, 'is_album': value.isAlbum, - 'genre': GenreToJSON(value.genre), + 'genre': value.genre, 'mood': MoodToJSON(value.mood), 'tags': value.tags, 'license': value.license, diff --git a/packages/sdk/src/sdk/api/generated/default/models/UpdateTrackRequestBody.ts b/packages/sdk/src/sdk/api/generated/default/models/UpdateTrackRequestBody.ts index 0190f13f780..7ce1ec6988b 100644 --- a/packages/sdk/src/sdk/api/generated/default/models/UpdateTrackRequestBody.ts +++ b/packages/sdk/src/sdk/api/generated/default/models/UpdateTrackRequestBody.ts @@ -25,12 +25,6 @@ import { FieldVisibilityFromJSONTyped, FieldVisibilityToJSON, } from './FieldVisibility'; -import type { Genre } from './Genre'; -import { - GenreFromJSON, - GenreFromJSONTyped, - GenreToJSON, -} from './Genre'; import type { Mood } from './Mood'; import { MoodFromJSON, @@ -63,11 +57,20 @@ export interface UpdateTrackRequestBody { */ title?: string; /** + * Music genre. Any string up to 100 characters is accepted. Known/canonical + * values (shown as autocomplete suggestions in clients): 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. * - * @type {Genre} + * @type {string} * @memberof UpdateTrackRequestBody */ - genre?: Genre; + genre?: string; /** * Track description * @type {string} @@ -264,7 +267,7 @@ export function UpdateTrackRequestBodyFromJSONTyped(json: any, ignoreDiscriminat return { 'title': !exists(json, 'title') ? undefined : json['title'], - 'genre': !exists(json, 'genre') ? undefined : GenreFromJSON(json['genre']), + 'genre': !exists(json, 'genre') ? undefined : json['genre'], 'description': !exists(json, 'description') ? undefined : json['description'], 'mood': !exists(json, 'mood') ? undefined : MoodFromJSON(json['mood']), 'bpm': !exists(json, 'bpm') ? undefined : json['bpm'], @@ -307,7 +310,7 @@ export function UpdateTrackRequestBodyToJSON(value?: UpdateTrackRequestBody | nu return { 'title': value.title, - 'genre': GenreToJSON(value.genre), + 'genre': value.genre, 'description': value.description, 'mood': MoodToJSON(value.mood), 'bpm': value.bpm, diff --git a/packages/sdk/src/sdk/api/generated/default/models/index.ts b/packages/sdk/src/sdk/api/generated/default/models/index.ts index d0273d53f81..2795a98bfd5 100644 --- a/packages/sdk/src/sdk/api/generated/default/models/index.ts +++ b/packages/sdk/src/sdk/api/generated/default/models/index.ts @@ -169,7 +169,6 @@ export * from './FollowNotificationAction'; export * from './FollowNotificationActionData'; export * from './FollowersResponse'; export * from './FollowingResponse'; -export * from './Genre'; export * from './GetChallenges'; export * from './GetSupportedUsers'; export * from './GetSupporter'; diff --git a/packages/sdk/src/sdk/api/playlists/PlaylistsApi.test.ts b/packages/sdk/src/sdk/api/playlists/PlaylistsApi.test.ts index 5670afd5202..3e284119559 100644 --- a/packages/sdk/src/sdk/api/playlists/PlaylistsApi.test.ts +++ b/packages/sdk/src/sdk/api/playlists/PlaylistsApi.test.ts @@ -8,7 +8,8 @@ import { EntityManagerClient } from '../../services/EntityManager' import { Logger } from '../../services/Logger' import { Storage } from '../../services/Storage' import { StorageNodeSelector } from '../../services/StorageNodeSelector' -import { Configuration, Mood, Genre } from '../generated/default' +import { Configuration, Mood } from '../generated/default' +import { Genre } from '../../types/Genre' import { PlaylistsApi as GeneratedPlaylistsApi } from '../generated/default/apis/PlaylistsApi' import type { PlaylistResponse } from '../generated/default/models/PlaylistResponse' import { TrackUploadHelper } from '../tracks/TrackUploadHelper' diff --git a/packages/sdk/src/sdk/api/playlists/types.ts b/packages/sdk/src/sdk/api/playlists/types.ts index bbd4a728db9..ac36cdbc518 100644 --- a/packages/sdk/src/sdk/api/playlists/types.ts +++ b/packages/sdk/src/sdk/api/playlists/types.ts @@ -6,7 +6,6 @@ import { DDEXResourceContributor, DDEXCopyright } from '../../types/DDEX' import { AudioFile, ImageFile } from '../../types/File' import { HashId } from '../../types/HashId' import { - Genre, Mood, type CreatePlaylistRequest, type UpdatePlaylistRequest @@ -93,7 +92,7 @@ export type EntityManagerCreatePlaylistRequest = z.input< export const UploadPlaylistMetadataSchema = CreatePlaylistMetadataSchema.extend( { - genre: z.enum(Object.values(Genre) as [Genre, ...Genre[]]) + genre: z.string().min(1).max(100) } ).strict() diff --git a/packages/sdk/src/sdk/api/tracks/TracksApi.test.ts b/packages/sdk/src/sdk/api/tracks/TracksApi.test.ts index d3bfc54f811..b3f19b604f5 100644 --- a/packages/sdk/src/sdk/api/tracks/TracksApi.test.ts +++ b/packages/sdk/src/sdk/api/tracks/TracksApi.test.ts @@ -143,7 +143,7 @@ describe('TracksApi', () => { trackCid: 'bafkreihzvsc5jqhxzdygntlqqd7kqtx3lul77d22v54a47m26n5q426z7i', title: 'BachGavotte', - genre: Genre.ELECTRONIC, + genre: Genre.Electronic, mood: Mood.TENDER }, audioFile: { @@ -191,7 +191,7 @@ describe('TracksApi', () => { }, metadata: { title: 'BachGavotte', - genre: Genre.ELECTRONIC, + genre: Genre.Electronic, mood: Mood.TENDER } }) diff --git a/packages/sdk/src/sdk/api/tracks/types.ts b/packages/sdk/src/sdk/api/tracks/types.ts index e5a78929c7d..383e159d507 100644 --- a/packages/sdk/src/sdk/api/tracks/types.ts +++ b/packages/sdk/src/sdk/api/tracks/types.ts @@ -24,7 +24,6 @@ import { HashId } from '../../types/HashId' import { StemCategory } from '../../types/StemCategory' import { Mood, - Genre, type UpdateTrackRequest, type CreateTrackRequest } from '../generated/default' @@ -135,7 +134,7 @@ export const UploadTrackMetadataSchema = z.object({ remixes: z.optional(z.boolean()) }) ), - genre: z.enum(Object.values(Genre) as [Genre, ...Genre[]]), + genre: z.string().min(1).max(100), isrc: z.optional(z.string().nullable()), isUnlisted: z.optional(z.boolean()), iswc: z.optional(z.string().nullable()), diff --git a/packages/sdk/src/sdk/index.ts b/packages/sdk/src/sdk/index.ts index d025b6cec76..728c5265dd0 100644 --- a/packages/sdk/src/sdk/index.ts +++ b/packages/sdk/src/sdk/index.ts @@ -6,6 +6,8 @@ export { } from './createSdkWithServices' export type { AudiusSdk } from './sdk' export * from './api/generated/default' +export { Genre } from './types/Genre' +export type { GenreString } from './types/Genre' export { TracksApi } from './api/tracks/TracksApi' export { PlaylistsApi } from './api/playlists/PlaylistsApi' export { AlbumsApi } from './api/albums/AlbumsApi' diff --git a/packages/sdk/src/sdk/types/Genre.ts b/packages/sdk/src/sdk/types/Genre.ts index 4b5b67a67df..05fa1466d65 100644 --- a/packages/sdk/src/sdk/types/Genre.ts +++ b/packages/sdk/src/sdk/types/Genre.ts @@ -1,56 +1,71 @@ +/** + * Track genre accepts any string up to 100 characters. This enum lists the + * canonical/known values used for autocomplete suggestions; arbitrary strings + * are also valid at the API and SDK layers. + * + * Key naming matches the previous generated-enum style (PascalCase) for + * backwards compatibility with downstream consumers (e.g. @audius/common, + * web, mobile). + */ export enum Genre { - ALL = 'All Genres', - ELECTRONIC = 'Electronic', - ROCK = 'Rock', - METAL = 'Metal', - ALTERNATIVE = 'Alternative', - HIP_HOP_RAP = 'Hip-Hop/Rap', - EXPERIMENTAL = 'Experimental', - PUNK = 'Punk', - FOLK = 'Folk', - POP = 'Pop', - AMBIENT = 'Ambient', - SOUNDTRACK = 'Soundtrack', - WORLD = 'World', - JAZZ = 'Jazz', - ACOUSTIC = 'Acoustic', - FUNK = 'Funk', - R_AND_B_SOUL = 'R&B/Soul', - DEVOTIONAL = 'Devotional', - CLASSICAL = 'Classical', - REGGAE = 'Reggae', - PODCASTS = 'Podcasts', - COUNTRY = 'Country', - SPOKEN_WORK = 'Spoken Word', - COMEDY = 'Comedy', - BLUES = 'Blues', - KIDS = 'Kids', - AUDIOBOOKS = 'Audiobooks', - LATIN = 'Latin', - LOFI = 'Lo-Fi', - HYPERPOP = 'Hyperpop', - DANCEHALL = 'Dancehall', + All = 'All Genres', + Electronic = 'Electronic', + Rock = 'Rock', + Metal = 'Metal', + Alternative = 'Alternative', + HipHopRap = 'Hip-Hop/Rap', + Experimental = 'Experimental', + Punk = 'Punk', + Folk = 'Folk', + Pop = 'Pop', + Ambient = 'Ambient', + Soundtrack = 'Soundtrack', + World = 'World', + Jazz = 'Jazz', + Acoustic = 'Acoustic', + Funk = 'Funk', + RbSoul = 'R&B/Soul', + Devotional = 'Devotional', + Classical = 'Classical', + Reggae = 'Reggae', + Podcasts = 'Podcasts', + Country = 'Country', + SpokenWord = 'Spoken Word', + Comedy = 'Comedy', + Blues = 'Blues', + Kids = 'Kids', + Audiobooks = 'Audiobooks', + Latin = 'Latin', + LoFi = 'Lo-Fi', + Hyperpop = 'Hyperpop', + Dancehall = 'Dancehall', // Electronic Subgenres - TECHNO = 'Techno', - TRAP = 'Trap', - HOUSE = 'House', - TECH_HOUSE = 'Tech House', - DEEP_HOUSE = 'Deep House', - DISCO = 'Disco', - ELECTRO = 'Electro', - JUNGLE = 'Jungle', - PROGRESSIVE_HOUSE = 'Progressive House', - HARDSTYLE = 'Hardstyle', - GLITCH_HOP = 'Glitch Hop', - TRANCE = 'Trance', - FUTURE_BASS = 'Future Bass', - FUTURE_HOUSE = 'Future House', - TROPICAL_HOUSE = 'Tropical House', - DOWNTEMPO = 'Downtempo', - DRUM_AND_BASS = 'Drum & Bass', - DUBSTEP = 'Dubstep', - JERSEY_CLUB = 'Jersey Club', - VAPORWAVE = 'Vaporwave', - MOOMBAHTON = 'Moombahton' + Techno = 'Techno', + Trap = 'Trap', + House = 'House', + TechHouse = 'Tech House', + DeepHouse = 'Deep House', + Disco = 'Disco', + Electro = 'Electro', + Jungle = 'Jungle', + ProgressiveHouse = 'Progressive House', + Hardstyle = 'Hardstyle', + GlitchHop = 'Glitch Hop', + Trance = 'Trance', + FutureBass = 'Future Bass', + FutureHouse = 'Future House', + TropicalHouse = 'Tropical House', + Downtempo = 'Downtempo', + DrumBass = 'Drum & Bass', + Dubstep = 'Dubstep', + JerseyClub = 'Jersey Club', + Vaporwave = 'Vaporwave', + Moombahton = 'Moombahton' } + +/** + * Track genre value type. Any string up to 100 characters is accepted at the + * API layer. The {@link Genre} enum provides canonical autocomplete values. + */ +export type GenreString = Genre | string diff --git a/packages/web/src/components/edit/fields/SelectGenreField.tsx b/packages/web/src/components/edit/fields/SelectGenreField.tsx index 89c3e999c87..d7847d78c2b 100644 --- a/packages/web/src/components/edit/fields/SelectGenreField.tsx +++ b/packages/web/src/components/edit/fields/SelectGenreField.tsx @@ -1,29 +1,39 @@ import { GENRES, convertGenreLabelToValue } from '@audius/common/utils' -import { SelectField, SelectFieldProps } from 'components/form-fields' +import { TextField, TextFieldProps } from 'components/form-fields' + +const GENRE_DATALIST_ID = 'genre-suggestions' +const GENRE_MAX_LENGTH = 100 const messages = { genre: 'Pick a Genre' } -type SelectGenreFieldProps = Partial & { +type SelectGenreFieldProps = Partial & { name: string } -const options = GENRES.map((genre) => ({ - value: convertGenreLabelToValue(genre), - label: genre -})) +const suggestions = Array.from( + new Set(GENRES.map((genre) => convertGenreLabelToValue(genre))) +) export const SelectGenreField = (props: SelectGenreFieldProps) => { return ( - + <> + + + {suggestions.map((value) => ( + + ) } From 17dd88861683787c8fb37a5184f8e7e7598bc1c2 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Fri, 29 May 2026 15:00:21 -0700 Subject: [PATCH 2/2] fix(sdk): order Genre import before generated/default in API tests Resolves import/order lint errors in AlbumsApi.test.ts and PlaylistsApi.test.ts that failed SDK CI. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/sdk/src/sdk/api/albums/AlbumsApi.test.ts | 2 +- packages/sdk/src/sdk/api/playlists/PlaylistsApi.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/sdk/api/albums/AlbumsApi.test.ts b/packages/sdk/src/sdk/api/albums/AlbumsApi.test.ts index af1f8e2eaab..57c04448781 100644 --- a/packages/sdk/src/sdk/api/albums/AlbumsApi.test.ts +++ b/packages/sdk/src/sdk/api/albums/AlbumsApi.test.ts @@ -20,8 +20,8 @@ import { import { SolanaClient } from '../../services/Solana/programs/SolanaClient' import { Storage } from '../../services/Storage' import { StorageNodeSelector } from '../../services/StorageNodeSelector' -import { Configuration, Mood } from '../generated/default' import { Genre } from '../../types/Genre' +import { Configuration, Mood } from '../generated/default' import { PlaylistsApi as GeneratedPlaylistsApi } from '../generated/default/apis/PlaylistsApi' import type { PlaylistResponse } from '../generated/default/models/PlaylistResponse' import { TrackUploadHelper } from '../tracks/TrackUploadHelper' diff --git a/packages/sdk/src/sdk/api/playlists/PlaylistsApi.test.ts b/packages/sdk/src/sdk/api/playlists/PlaylistsApi.test.ts index 3e284119559..fb379387311 100644 --- a/packages/sdk/src/sdk/api/playlists/PlaylistsApi.test.ts +++ b/packages/sdk/src/sdk/api/playlists/PlaylistsApi.test.ts @@ -8,8 +8,8 @@ import { EntityManagerClient } from '../../services/EntityManager' import { Logger } from '../../services/Logger' import { Storage } from '../../services/Storage' import { StorageNodeSelector } from '../../services/StorageNodeSelector' -import { Configuration, Mood } from '../generated/default' import { Genre } from '../../types/Genre' +import { Configuration, Mood } from '../generated/default' import { PlaylistsApi as GeneratedPlaylistsApi } from '../generated/default/apis/PlaylistsApi' import type { PlaylistResponse } from '../generated/default/models/PlaylistResponse' import { TrackUploadHelper } from '../tracks/TrackUploadHelper'