diff --git a/packages/sdk/src/sdk/api/tracks/TrackUploadHelper.ts b/packages/sdk/src/sdk/api/tracks/TrackUploadHelper.ts index c923f7facce..28077dcc98f 100644 --- a/packages/sdk/src/sdk/api/tracks/TrackUploadHelper.ts +++ b/packages/sdk/src/sdk/api/tracks/TrackUploadHelper.ts @@ -24,15 +24,51 @@ export class TrackUploadHelper extends BaseAPI { return id } - public transformTrackUploadMetadata( - inputMetadata: CreateTrackRequestBody | UpdateTrackRequestBody, - userId: number - ) { + public transformTrackUploadMetadata< + // TrackMetadata is a less strict type + // only requiring the fields used in this function. + // This supports both track/playlist uploads and edits + TrackMetadata extends Pick< + PlaylistTrackMetadata, + 'isStreamGated' | 'streamConditions' | 'isUnlisted' | 'fieldVisibility' + > + >(inputMetadata: TrackMetadata, userId: number) { const metadata = { ...inputMetadata, ownerId: userId } + const isStreamGated = metadata.isStreamGated + const isUsdcGated = 'usdc_purchase' in (metadata.streamConditions ?? {}) + const isUnlisted = metadata.isUnlisted + + // If track is stream gated and not usdc purchase gated, set remixes to false + if (isStreamGated && !isUsdcGated && metadata.fieldVisibility) { + metadata.fieldVisibility.remixes = false + } + + // If track is public, set required visibility fields to true + if (!isUnlisted) { + metadata.fieldVisibility = { + ...metadata.fieldVisibility, + genre: true, + mood: true, + tags: true, + share: true, + playCount: true + } + } + return metadata + } + + public transformTrackUploadMetadataV2< + T extends CreateTrackRequestBody | UpdateTrackRequestBody + >(inputMetadata: T, userId: number) { + const metadata: T = { + ...inputMetadata, + ownerId: userId + } + const isStreamGated = metadata.streamConditions !== undefined const isUsdcGated = 'usdc_purchase' in (metadata.streamConditions ?? {}) const isUnlisted = metadata.isUnlisted @@ -57,8 +93,10 @@ export class TrackUploadHelper extends BaseAPI { return metadata } - public populateTrackMetadataWithUploadResponse( - trackMetadata: CreateTrackRequestBody | UpdateTrackRequestBody, + public populateTrackMetadataWithUploadResponseV2< + T extends CreateTrackRequestBody | UpdateTrackRequestBody + >( + trackMetadata: T, audioResponse?: UploadResponse, coverArtResponse?: UploadResponse ) { @@ -96,18 +134,44 @@ export class TrackUploadHelper extends BaseAPI { return updated } - public extractMediorumUploadOptions(metadata: PlaylistTrackMetadata) { - const uploadOptions: { [key: string]: string } = {} - if ( - metadata.previewStartSeconds !== undefined && - metadata.previewStartSeconds !== null - ) { - uploadOptions.previewStartSeconds = - metadata.previewStartSeconds.toString() + public populateTrackMetadataWithUploadResponse( + trackMetadata: Partial, + audioResponse?: UploadResponse, + coverArtResponse?: UploadResponse + ) { + let updated: Partial & { coverArtSizes?: string } = { + ...trackMetadata } - if (metadata.placementHosts) { - uploadOptions.placement_hosts = metadata.placementHosts + if (audioResponse) { + updated = { + ...updated, + trackCid: audioResponse.results['320'], + previewCid: + trackMetadata.previewStartSeconds !== undefined && + trackMetadata.previewStartSeconds !== null + ? audioResponse.results[ + `320_preview|${trackMetadata.previewStartSeconds}` + ] + : trackMetadata.previewCid, + origFileCid: audioResponse.orig_file_cid, + origFilename: audioResponse.orig_filename || trackMetadata.origFilename, + audioUploadId: audioResponse.id, + duration: parseInt(audioResponse?.probe?.format?.duration ?? '0', 10), + bpm: audioResponse.audio_analysis_results?.bpm + ? audioResponse.audio_analysis_results.bpm + : trackMetadata.bpm, + musicalKey: audioResponse.audio_analysis_results?.key + ? audioResponse.audio_analysis_results.key + : trackMetadata.musicalKey, + audioAnalysisErrorCount: audioResponse.audio_analysis_error_count || 0 + } } - return uploadOptions + if (coverArtResponse) { + updated = { + ...updated, + coverArtSizes: coverArtResponse.orig_file_cid + } + } + return updated } } diff --git a/packages/sdk/src/sdk/api/tracks/TracksApi.ts b/packages/sdk/src/sdk/api/tracks/TracksApi.ts index ddfd73c8b86..fc0212f7b8c 100644 --- a/packages/sdk/src/sdk/api/tracks/TracksApi.ts +++ b/packages/sdk/src/sdk/api/tracks/TracksApi.ts @@ -51,7 +51,6 @@ import { EntityManagerUnfavoriteTrackRequest, UnfavoriteTrackSchema, EntityManagerUpdateTrackRequest, - UploadTrackRequest, PurchaseTrackRequest, PurchaseTrackSchema, GetPurchaseTrackInstructionsRequest, @@ -59,7 +58,6 @@ import { EntityManagerRecordTrackDownloadRequest, RecordTrackDownloadSchema, UploadTrackFilesRequest, - UploadTrackSchema, UpdateTrackSchema, UploadTrackFilesSchema, ShareTrackSchema, @@ -67,9 +65,11 @@ import { type PublishTrackRequest, PublishTrackSchema, type PublishStemRequest, - PublishStemSchema, type UploadTrackFilesTask, - type UpdateTrackRequestWithFiles + type UpdateTrackRequestWithFiles, + type CreateTrackRequestWithFiles, + PublishStemSchema, + UploadTrackSchema } from './types' // Extend that new class @@ -247,7 +247,11 @@ export class TracksApi extends GeneratedTracksApi { imageUploadResponse ) - return this.writeTrackToChain(userId, populatedMetadata, advancedOptions) + return this.writeTrackToChain( + params.userId, + populatedMetadata, + advancedOptions + ) } /** @hidden @@ -269,7 +273,6 @@ export class TracksApi extends GeneratedTracksApi { streamConditions: undefined, isUnlisted: false, fieldVisibility: { - remixes: false, genre: false, mood: false, tags: false, @@ -291,14 +294,18 @@ export class TracksApi extends GeneratedTracksApi { audioUploadResponse ) - return this.writeTrackToChain(userId, populatedMetadata, advancedOptions) + return this.writeTrackToChain( + params.userId, + populatedMetadata, + advancedOptions + ) } /** @hidden * Write track upload to chain */ async writeTrackToChain( - userId: number, + userId: string, metadata: ReturnType< typeof this.trackUploadHelper.populateTrackMetadataWithUploadResponse >, @@ -308,12 +315,16 @@ export class TracksApi extends GeneratedTracksApi { this.logger.info('Writing metadata to chain') const entityId = - 'trackId' in metadata && metadata.trackId - ? metadata.trackId - : await this.trackUploadHelper.generateId('track') + metadata.trackId || (await this.trackUploadHelper.generateId('track')) + + const decodedUserId = decodeHashId(userId) ?? undefined + + if (!decodedUserId) { + throw new Error('writeTrackToChain: userId could not be decoded') + } const response = await this.entityManager.manageEntity({ - userId, + userId: decodedUserId, entityType: EntityType.TRACK, entityId, action: Action.CREATE, @@ -321,7 +332,7 @@ export class TracksApi extends GeneratedTracksApi { cid: '', data: { ...snakecaseKeys(metadata), - owner_id: userId, + owner_id: decodedUserId, download_conditions: metadata.downloadConditions && snakecaseKeys(metadata.downloadConditions), @@ -341,17 +352,12 @@ export class TracksApi extends GeneratedTracksApi { } } - /** - * Upload a track - */ - async uploadTrack( - params: UploadTrackRequest, - advancedOptions?: AdvancedOptions + override async createTrack( + params: CreateTrackRequestWithFiles, + requestInit?: RequestInit ) { - // Validate inputs - await parseParams('uploadTrack', UploadTrackSchema)(params) - - // Upload track files + // Upload files + let metadata = params.metadata const { audioUploadResponse, imageUploadResponse } = await this.uploadTrackFiles({ audioFile: params.audioFile, @@ -363,19 +369,30 @@ export class TracksApi extends GeneratedTracksApi { onProgress: params.onProgress }).start() - if (!audioUploadResponse || !imageUploadResponse) { - throw new Error('uploadTrack: Missing upload responses') - } + metadata = this.trackUploadHelper.transformTrackUploadMetadata( + metadata, + decodeHashId(params.userId)! + ) - // Write track metadata to chain - return this.publishTrack( + metadata = this.trackUploadHelper.populateTrackMetadataWithUploadResponseV2( + metadata, + audioUploadResponse, + imageUploadResponse + ) + + if (this.entityManager) { + const { metadata } = await parseParams( + 'createTrack', + UploadTrackSchema + )(params) + return this.writeTrackToChain(params.userId, metadata) + } + return super.createTrack( { userId: params.userId, - metadata: params.metadata, - audioUploadResponse, - imageUploadResponse + metadata }, - advancedOptions + requestInit ) } @@ -387,62 +404,10 @@ export class TracksApi extends GeneratedTracksApi { advancedOptions?: AdvancedOptions ) { // Parse inputs - const { - userId, - trackId, - audioFile, - imageFile, - metadata: parsedMetadata, - onProgress, - generatePreview - } = await parseParams('updateTrack', UpdateTrackSchema)(params) - - // Transform metadata - const metadata = this.trackUploadHelper.transformTrackUploadMetadata( - parsedMetadata, - userId - ) - - const { audioUploadResponse, imageUploadResponse } = - await this.uploadTrackFiles({ - audioFile, - imageFile, - fileMetadata: { - placementHosts: parsedMetadata.placementHosts, - previewStartSeconds: parsedMetadata.previewStartSeconds - }, - onProgress - }).start() - - // Update metadata to include uploaded CIDs - const updatedMetadata = - this.trackUploadHelper.populateTrackMetadataWithUploadResponse( - metadata, - audioUploadResponse, - imageUploadResponse - ) - - // Generate preview if requested and no audio file was uploaded - // (as that would handle the preview generation already) - if (generatePreview && !audioFile) { - if (updatedMetadata.previewStartSeconds === undefined) { - throw new Error('No track preview start time specified') - } - - const previewCid = await retry3( - async () => - await this.storage.generatePreview({ - cid: updatedMetadata.trackCid!, - secondOffset: updatedMetadata.previewStartSeconds! - }), - (e) => { - this.logger.info('Retrying generatePreview', e) - } - ) - - // Update metadata to include updated preview CID - updatedMetadata.previewCid = previewCid - } + const { userId, trackId, metadata } = await parseParams( + 'updateTrack', + UpdateTrackSchema + )(params) // Write metadata to chain return await this.entityManager.manageEntity({ @@ -453,13 +418,13 @@ export class TracksApi extends GeneratedTracksApi { metadata: JSON.stringify({ cid: '', data: { - ...snakecaseKeys(updatedMetadata), + ...snakecaseKeys(metadata), download_conditions: - updatedMetadata.downloadConditions && - snakecaseKeys(updatedMetadata.downloadConditions), + metadata.downloadConditions && + snakecaseKeys(metadata.downloadConditions), stream_conditions: - updatedMetadata.streamConditions && - snakecaseKeys(updatedMetadata.streamConditions), + metadata.streamConditions && + snakecaseKeys(metadata.streamConditions), stem_of: metadata.stemOf && snakecaseKeys(metadata.stemOf) } }), @@ -473,28 +438,44 @@ export class TracksApi extends GeneratedTracksApi { ) { // Upload files let metadata = params.metadata - if (params.audioFile || params.imageFile) { - const { audioUploadResponse, imageUploadResponse } = - await this.uploadTrackFiles({ - audioFile: params.audioFile, - imageFile: params.imageFile, - fileMetadata: { - placementHosts: metadata.placementHosts, - previewStartSeconds: metadata.previewStartSeconds - }, - onProgress: params.onProgress - }).start() - - metadata = this.trackUploadHelper.transformTrackUploadMetadata( - metadata, - decodeHashId(params.userId)! - ) + const { audioUploadResponse, imageUploadResponse } = + await this.uploadTrackFiles({ + audioFile: params.audioFile, + imageFile: params.imageFile, + fileMetadata: { + placementHosts: params.metadata.placementHosts, + previewStartSeconds: params.metadata.previewStartSeconds + }, + onProgress: params.onProgress + }).start() - metadata = this.trackUploadHelper.populateTrackMetadataWithUploadResponse( - metadata, - audioUploadResponse, - imageUploadResponse + metadata = this.trackUploadHelper.transformTrackUploadMetadataV2( + metadata, + decodeHashId(params.userId)! + ) + + metadata = this.trackUploadHelper.populateTrackMetadataWithUploadResponseV2( + metadata, + audioUploadResponse, + imageUploadResponse + ) + + // Generate preview if requested and no audio file was uploaded + // (as that would handle the preview generation already) + if (metadata.previewStartSeconds !== undefined && !params.audioFile) { + const previewCid = await retry3( + async () => + await this.storage.generatePreview({ + cid: metadata.trackCid!, + secondOffset: metadata.previewStartSeconds! + }), + (e) => { + this.logger.info('Retrying generatePreview', e) + } ) + + // Update metadata to include updated preview CID + metadata.previewCid = previewCid } if (this.entityManager) { @@ -512,7 +493,7 @@ export class TracksApi extends GeneratedTracksApi { { trackId: params.trackId, userId: params.userId, - metadata + metadata: params.metadata }, requestInit ) diff --git a/packages/sdk/src/sdk/api/tracks/types.ts b/packages/sdk/src/sdk/api/tracks/types.ts index 57b92133127..4fd479a61d7 100644 --- a/packages/sdk/src/sdk/api/tracks/types.ts +++ b/packages/sdk/src/sdk/api/tracks/types.ts @@ -98,7 +98,7 @@ export const UploadStemMetadataSchema = z.object({ category: z .enum(Object.values(StemCategory) as [StemCategory, ...StemCategory[]]) .default(StemCategory.Other), - parentTrackId: HashId.or(z.number()) + parentTrackId: HashId }) export const UploadTrackMetadataSchema = z.object({ @@ -150,7 +150,7 @@ export const UploadTrackMetadataSchema = z.object({ tracks: z .array( z.object({ - parentTrackId: HashId.or(z.number()) + parentTrackId: HashId }) ) .min(1) @@ -493,6 +493,4 @@ export type CreateTrackRequestWithFiles = CreateTrackRequest & TrackFileUploadParams export type UpdateTrackRequestWithFiles = UpdateTrackRequest & - TrackFileUploadParams & { - generatePreview?: boolean - } + TrackFileUploadParams