diff --git a/.changeset/stems-zip-download-url.md b/.changeset/stems-zip-download-url.md new file mode 100644 index 00000000000..6345ef3f65e --- /dev/null +++ b/.changeset/stems-zip-download-url.md @@ -0,0 +1,5 @@ +--- +'@audius/sdk': minor +--- + +Add `tracks.getTrackStemsDownloadUrl` for downloading a track's stems as one zip diff --git a/packages/common/src/api/tan-query/queryKeys.ts b/packages/common/src/api/tan-query/queryKeys.ts index e2c83b739bd..447dde44478 100644 --- a/packages/common/src/api/tan-query/queryKeys.ts +++ b/packages/common/src/api/tan-query/queryKeys.ts @@ -11,8 +11,6 @@ export const QUERY_KEYS = { comment: 'comment', commentReplies: 'commentReplies', computedProps: 'computedProps', - downloadTrackStems: 'downloadTrackStems', - stemsArchiveJob: 'stemsArchiveJob', exploreContent: 'exploreContent', trackCommentNotificationSetting: 'trackCommentNotificationSetting', trackCommentCount: 'trackCommentCount', diff --git a/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts b/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts index 6f68c64251c..fae80a29ff3 100644 --- a/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts +++ b/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts @@ -1,69 +1,29 @@ -import { useEffect, useState } from 'react' - -import { Id } from '@audius/sdk' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { Id, OptionalId } from '@audius/sdk' +import { useMutation } from '@tanstack/react-query' import { useQueryContext } from '~/api/tan-query/utils' import { ID } from '~/models/Identifiers' -import { QUERY_KEYS } from '../queryKeys' -import { QueryKey, QueryOptions } from '../types' import { useCurrentUserId } from '../users/account/useCurrentUserId' import { useTrack } from './useTrack' -// Stop polling the archive job after this long even if it never transitions -// out of `active`, so the UI can surface an error instead of spinning forever. -// -// Sized for the large end of real stem sets: a contest track can carry dozens -// of lossless stems totalling multiple GB, and WAV barely compresses, so the -// server-side zip legitimately runs for many minutes. The previous 5 minute -// budget expired before those archives could finish and reported a failure for -// a job that was still making progress. -const STEMS_ARCHIVE_POLL_TIMEOUT_MS = 900_000 // 15 minutes - -type GetStemsArchiveJobStatusResponse = { - id: string - state: - | 'completed' - | 'failed' - | 'active' - | 'waiting' - | 'delayed' - | 'prioritized' - progress?: number - failedReason?: string -} - -export const getStemsArchiveJobQueryKey = (jobId?: string) => { - return [ - QUERY_KEYS.stemsArchiveJob, - jobId - ] as unknown as QueryKey -} - -export const getDownloadTrackStemsQueryKey = (trackId: ID) => { - return [ - QUERY_KEYS.downloadTrackStems, - trackId - ] as unknown as QueryKey -} - +/** + * Builds a signed url for a zip of the track's stems. The API streams the zip + * as it fetches each file, so callers hand the url to the platform's + * downloader instead of waiting on a job. + */ export const useDownloadTrackStems = ({ trackId }: { trackId: ID }) => { - const { audiusSdk } = useQueryContext() - const queryClient = useQueryClient() + const { audiusSdk, audiusBackend } = useQueryContext() const { data: currentUserId } = useCurrentUserId() - // Whether the parent track can be bundled into the archive. Two separate + // Whether the parent track can be bundled into the zip. Two separate // conditions, and both matter: // - `is_downloadable` — the artist actually offers the full track. If - // this is false there is no downloadable parent file at all and its - // download URL 404s. + // this is false there is no downloadable parent file at all. // - `access.download` — the *gating* check ("this user is allowed to // download"), which is `true` for any ungated track regardless of // whether a downloadable file exists. - // Checking only the latter asks the archiver to include a file that isn't - // there, which is how stem archives for stem-only tracks broke. const { data: parentDownloadability } = useTrack(trackId, { select: (track) => ({ isDownloadable: track?.is_downloadable === true, @@ -74,117 +34,22 @@ export const useDownloadTrackStems = ({ trackId }: { trackId: ID }) => { return useMutation({ mutationFn: async () => { const sdk = await audiusSdk() - const archiver = sdk.services.archiverService - if (!archiver) { - throw new Error('Archiver service not configured') - } - if (!currentUserId) { - throw new Error('Current user ID is required') - } - - const includeParent = - parentDownloadability?.isDownloadable === true && - parentDownloadability?.hasDownloadAccess === true + const { data, signature } = await audiusBackend.signGatedContentRequest({ + sdk + }) - return await archiver.createStemsArchive({ + return await sdk.tracks.getTrackStemsDownloadUrl({ trackId: Id.parse(trackId), - userId: Id.parse(currentUserId), - includeParent + userId: OptionalId.parse(currentUserId), + userSignature: signature, + userData: data, + includeParent: + parentDownloadability?.isDownloadable === true && + parentDownloadability?.hasDownloadAccess === true }) }, - onSuccess: async (response) => { - queryClient.setQueryData(getDownloadTrackStemsQueryKey(trackId), response) - queryClient.setQueryData( - getStemsArchiveJobQueryKey(response.id), - response - ) - }, onError: (error) => { console.error(error) } }) } - -export const useCancelStemsArchiveJob = () => { - const { audiusSdk } = useQueryContext() - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async ({ jobId }: { jobId: string }) => { - const sdk = await audiusSdk() - const archiver = sdk.services.archiverService - if (!archiver) { - throw new Error('Archiver service not configured') - } - await archiver.cancelStemsArchiveJob({ jobId }) - return jobId - }, - onSuccess: (jobId) => { - queryClient.removeQueries({ - queryKey: getStemsArchiveJobQueryKey(jobId), - exact: true - }) - } - }) -} - -export const useGetStemsArchiveJobStatus = ( - { jobId }: { jobId?: string }, - options?: QueryOptions -) => { - const { audiusSdk } = useQueryContext() - - // Hard stop for a job that never transitions out of `active`. This has to be - // real state rather than a ref: returning `false` from `refetchInterval` - // silently stops polling without re-rendering, and the job state stays - // `active` forever, so callers had no way to tell a stalled job from an - // in-progress one and would spin indefinitely. - const [isTimedOut, setIsTimedOut] = useState(false) - - const query = useQuery({ - queryKey: getStemsArchiveJobQueryKey(jobId), - queryFn: async () => { - if (!jobId) { - throw new Error('Job ID is required') - } - const sdk = await audiusSdk() - const archiver = sdk.services.archiverService - if (!archiver) { - throw new Error('Archiver service not configured') - } - return await archiver.getStemsArchiveJobStatus({ jobId }) - }, - // refetch once per second until the job is completed, failed, or we give up - refetchInterval: (query) => { - if (isTimedOut) { - return false - } - if (!query.state.data) { - return 1000 - } - if (['completed', 'failed'].includes(query.state.data.state)) { - return false - } - return 1000 - }, - staleTime: 0, - gcTime: 0, - enabled: !!jobId, - ...options - }) - - const jobState = query.data?.state - const isSettled = jobState === 'completed' || jobState === 'failed' - - useEffect(() => { - setIsTimedOut(false) - if (!jobId || isSettled) return - const timer = setTimeout( - () => setIsTimedOut(true), - STEMS_ARCHIVE_POLL_TIMEOUT_MS - ) - return () => clearTimeout(timer) - }, [jobId, isSettled]) - - return { ...query, isTimedOut } -} diff --git a/packages/common/src/services/env.ts b/packages/common/src/services/env.ts index fd130b09955..90c5764fdc4 100644 --- a/packages/common/src/services/env.ts +++ b/packages/common/src/services/env.ts @@ -9,7 +9,6 @@ export type Env = { API_URL: string APP_NAME: string API_KEY: string - ARCHIVE_ENDPOINT: string AUDIUS_NETWORK_CHAIN_ID: number AUDIUS_URL: string BITSKI_CALLBACK_URL: string diff --git a/packages/mobile/src/components/download-track-archive-drawer/DownloadTrackArchiveDrawer.tsx b/packages/mobile/src/components/download-track-archive-drawer/DownloadTrackArchiveDrawer.tsx index 2805a6b9801..19cd345d785 100644 --- a/packages/mobile/src/components/download-track-archive-drawer/DownloadTrackArchiveDrawer.tsx +++ b/packages/mobile/src/components/download-track-archive-drawer/DownloadTrackArchiveDrawer.tsx @@ -1,11 +1,6 @@ import { useCallback, useEffect, useState } from 'react' -import { - useCancelStemsArchiveJob, - useDownloadTrackStems, - useGetStemsArchiveJobStatus, - useTrack -} from '@audius/common/api' +import { useDownloadTrackStems, useTrack } from '@audius/common/api' import { useAppContext } from '@audius/common/context' import type { ID } from '@audius/common/models' import type { DownloadFile } from '@audius/common/services' @@ -21,14 +16,13 @@ import { TextLink } from '@audius/harmony-native' import Drawer from 'app/components/drawer' -import { env } from 'app/services/env' import { DrawerHeader } from '../core/DrawerHeader' import LoadingSpinner from '../loading-spinner' const messages = { title: 'Downloading...', - zippingFiles: (count: number) => `Zipping files (${count})`, + files: (count: number) => `${count} ${count === 1 ? 'file' : 'files'}`, error: 'Something went wrong. Please check your connection and try again.', tryAgain: 'Try again.' } @@ -98,16 +92,9 @@ const DownloadTrackArchiveDrawerContent = ({ const { data: trackTitle } = useTrack(trackId, { select: (track) => track.title }) - const [step, setStep] = useState<'downloading' | 'zipping'>('zipping') - const { - mutate: downloadTrackStems, - isError: initiateDownloadFailed, - isPending: isStartingDownload, - data: { id: jobId } = {} - } = useDownloadTrackStems({ - trackId - }) + const { mutateAsync: getStemsZipUrl, isError: urlFailed } = + useDownloadTrackStems({ trackId }) const { downloadFile, @@ -117,49 +104,22 @@ const DownloadTrackArchiveDrawerContent = ({ cancel: cancelDownload } = useDownloadFile() - const { mutate: cancelStemsArchiveJob } = useCancelStemsArchiveJob() - - const { - data: jobState, - isError: isJobStatusError, - isTimedOut: isJobTimedOut - } = useGetStemsArchiveJobStatus({ - jobId - }) + const hasError = urlFailed || !!downloadError - // `isTimedOut` and `isError` have to be part of this, not just `failed`. - // A job that never leaves `waiting` — the archiver worker losing its Redis - // lock and holding every concurrency slot, for instance — is reported as a - // perfectly valid non-terminal state forever, so keying only off `failed` - // leaves the drawer spinning with no error and no retry until the user - // gives up. The shared hook already enforces STEMS_ARCHIVE_POLL_TIMEOUT_MS - // and hands back `isTimedOut`; web consumes it and mobile did not. - const hasError = - !isStartingDownload && - (downloadError || - initiateDownloadFailed || - jobState?.state === 'failed' || - (!!jobId && (isJobStatusError || isJobTimedOut))) - - useEffect(() => { - downloadTrackStems() - }, [downloadTrackStems]) + const startDownload = useCallback(async () => { + if (!trackTitle) return + resetDownload() + try { + const url = await getStemsZipUrl() + await downloadFile({ file: { url, filename: `${trackTitle}.zip` } }) + } catch (e) { + console.error('Failed to start stems download', e) + } + }, [trackTitle, getStemsZipUrl, downloadFile, resetDownload]) useEffect(() => { - if (jobState?.state === 'completed' && trackTitle) { - const fetchResult = async () => { - setStep('downloading') - await downloadFile({ - file: { - url: `${env.ARCHIVE_ENDPOINT}/archive/stems/download/${jobId}`, - filename: `${trackTitle}.zip` - } - }) - onClose() - } - fetchResult() - } - }, [jobState, onClose, jobId, downloadFile, trackTitle]) + startDownload() + }, [startDownload]) // Close drawer automatically if download was successful useEffect(() => { @@ -169,18 +129,9 @@ const DownloadTrackArchiveDrawerContent = ({ }, [downloadSuccess, onClose]) const handleClose = useCallback(() => { - if (jobId) { - cancelStemsArchiveJob({ jobId }) - } cancelDownload() onClose() - }, [onClose, jobId, cancelStemsArchiveJob, cancelDownload]) - - const handleRetry = useCallback(() => { - setStep('zipping') - resetDownload() - downloadTrackStems() - }, [downloadTrackStems, resetDownload]) + }, [onClose, cancelDownload]) return ( @@ -188,25 +139,17 @@ const DownloadTrackArchiveDrawerContent = ({ - {step === 'zipping' ? ( - <> - - - {messages.zippingFiles(fileCount)} - - - ) : ( - - {`${trackTitle}.zip`} - - )} + + + {messages.files(fileCount)} + {hasError ? ( + {messages.tryAgain} } diff --git a/packages/mobile/src/services/env/env.dev.ts b/packages/mobile/src/services/env/env.dev.ts index ebd1a8021b4..70d70a05462 100644 --- a/packages/mobile/src/services/env/env.dev.ts +++ b/packages/mobile/src/services/env/env.dev.ts @@ -8,7 +8,6 @@ export const env: Env = { API_KEY: '2dc52ec9a4c31790cab6653de0c637f680faa993', API_URL: process.env.VITE_API_URL ?? 'http://audius-api', APP_NAME: 'audius-client', - ARCHIVE_ENDPOINT: process.env.VITE_API_URL ?? 'http://audius-api', AUDIUS_NETWORK_CHAIN_ID: 1337, AUDIUS_URL: 'https://audius.co', BITSKI_CALLBACK_URL: 'https://audius.co/bitski-callback.html', diff --git a/packages/mobile/src/services/env/env.prod.ts b/packages/mobile/src/services/env/env.prod.ts index 635251d6c8f..ed183e744d3 100644 --- a/packages/mobile/src/services/env/env.prod.ts +++ b/packages/mobile/src/services/env/env.prod.ts @@ -8,7 +8,6 @@ export const env: Env = { API_KEY: '8acf5eb7436ea403ee536a7334faa5e9ada4b50f', API_URL: process.env.VITE_API_URL ?? 'https://api.audius.co', APP_NAME: 'audius-client', - ARCHIVE_ENDPOINT: 'https://api.audius.co', AUDIUS_NETWORK_CHAIN_ID: 31524, AUDIUS_URL: 'https://audius.co', BITSKI_CALLBACK_URL: 'https://audius.co/bitski-callback.html', diff --git a/packages/mobile/src/services/sdk/audius-sdk.ts b/packages/mobile/src/services/sdk/audius-sdk.ts index 707a13ad216..63c5739a372 100644 --- a/packages/mobile/src/services/sdk/audius-sdk.ts +++ b/packages/mobile/src/services/sdk/audius-sdk.ts @@ -4,8 +4,7 @@ import { Configuration, SolanaRelay, createSdkWithServices, - type AudiusSdkWithServices, - ArchiverService + type AudiusSdkWithServices } from '@audius/sdk' import { env } from 'app/services/env' @@ -40,21 +39,6 @@ const initSdk = async () => { }) ) - const archiverService = new ArchiverService( - new Configuration({ - basePath: '/archive', - middleware: [ - { - pre: async (context) => { - const endpoint = env.ARCHIVE_ENDPOINT - const url = `${endpoint}${context.url}` - return { url, init: context.init } - } - } - ] - }) - ) - // Overrides some DN configuration from optimizely const audiusWalletClient = await getAudiusWalletClient() @@ -64,8 +48,7 @@ const initSdk = async () => { environment: env.ENVIRONMENT, services: { solanaRelay, - audiusWalletClient, - archiverService + audiusWalletClient } }) sdkInstance = audiusSdk diff --git a/packages/sdk/src/sdk/api/tracks/TracksApi.ts b/packages/sdk/src/sdk/api/tracks/TracksApi.ts index c005ff56fc5..580a812905e 100644 --- a/packages/sdk/src/sdk/api/tracks/TracksApi.ts +++ b/packages/sdk/src/sdk/api/tracks/TracksApi.ts @@ -169,6 +169,42 @@ export class TracksApi extends GeneratedTracksApi { }` } + /** + * Get the url of a zip of the track's stems, optionally with the parent + * track. The API streams the zip, so the url can be handed straight to a + * browser or file downloader. + */ + async getTrackStemsDownloadUrl(params: { + trackId: string + userId?: string + userSignature?: string + userData?: string + includeParent?: boolean + }): Promise { + if (params.trackId === null || params.trackId === undefined) { + throw new RequiredError( + 'trackId', + 'Required parameter params.trackId was null or undefined when calling getTrackStemsDownloadUrl.' + ) + } + + const queryParams = new URLSearchParams() + if (params.userId) queryParams.append('user_id', params.userId) + if (params.userSignature) + queryParams.append('user_signature', params.userSignature) + if (params.userData) queryParams.append('user_data', params.userData) + if (params.includeParent) queryParams.append('include_parent', 'true') + + const path = `/tracks/{track_id}/stems/download`.replace( + `{${'track_id'}}`, + encodeURIComponent(String(params.trackId)) + ) + const queryString = queryParams.toString() + return `${this.configuration.basePath}${path}${ + queryString ? '?' + queryString : '' + }` + } + /** @hidden * Upload track files, does not write to chain */ diff --git a/packages/web/src/app/registerNiceModals.ts b/packages/web/src/app/registerNiceModals.ts index 2ebda3155c6..718d03f54e8 100644 --- a/packages/web/src/app/registerNiceModals.ts +++ b/packages/web/src/app/registerNiceModals.ts @@ -183,13 +183,6 @@ registerLazyModal('DeleteTrackConfirmation', () => default: m.DeleteTrackConfirmationModal })) ) -registerLazyModal('DownloadTrackArchive', () => - import( - 'components/download-track-archive-modal/DownloadTrackArchiveModal' - ).then((m) => ({ - default: m.DownloadTrackArchiveModal - })) -) registerLazyModal('DuplicateAddConfirmation', () => import( 'components/duplicate-add-confirmation-modal/DuplicateAddConfirmationModal' diff --git a/packages/web/src/components/download-track-archive-modal/DownloadTrackArchiveModal.tsx b/packages/web/src/components/download-track-archive-modal/DownloadTrackArchiveModal.tsx deleted file mode 100644 index 8a9ef6e7507..00000000000 --- a/packages/web/src/components/download-track-archive-modal/DownloadTrackArchiveModal.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { useCallback, useEffect } from 'react' - -import { - useCancelStemsArchiveJob, - useDownloadTrackStems, - useGetStemsArchiveJobStatus -} from '@audius/common/api' -import { ID } from '@audius/common/models' -import { useDownloadTrackArchiveModal } from '@audius/common/store' -import { - Modal, - ModalContent, - ModalHeader, - Text, - Flex, - LoadingSpinner, - IconFolder, - ModalTitle, - IconReceive, - Hint, - IconError, - TextLink -} from '@audius/harmony' -import NiceModal, { useModal } from '@ebay/nice-modal-react' - -import { env } from 'services/env' - -const messages = { - title: 'Preparing Download', - zippingFiles: (count: number) => `Zipping files (${count})`, - error: 'Something went wrong. Please check your connection and try again.', - tryAgain: 'Try again.' -} - -const triggerDownload = (url: string) => { - if (document) { - const link = document.createElement('a') - link.href = url - link.click() - link.remove() - } else { - throw new Error('No document found') - } -} - -type DownloadTrackArchiveModalContentProps = { - trackId: ID - fileCount: number - isOpen: boolean - onClose: () => void - onClosed: () => void -} -const DownloadTrackArchiveModalContent = ({ - trackId, - fileCount, - isOpen, - onClose, - onClosed -}: DownloadTrackArchiveModalContentProps) => { - const { - mutate: downloadTrackStems, - isError: initiateDownloadFailed, - isPending: isStartingDownload, - data: { id: jobId } = {} - } = useDownloadTrackStems({ - trackId - }) - - const { mutate: cancelStemsArchiveJob } = useCancelStemsArchiveJob() - - const { - data: jobStatus, - isError: isJobStatusError, - isTimedOut: isJobTimedOut - } = useGetStemsArchiveJobStatus({ - jobId - }) - - const hasError = - !isStartingDownload && - (initiateDownloadFailed || - jobStatus?.state === 'failed' || - (!!jobId && (isJobStatusError || isJobTimedOut))) - - useEffect(() => { - downloadTrackStems() - }, [downloadTrackStems]) - - useEffect(() => { - if (jobStatus?.state === 'completed') { - triggerDownload(`${env.ARCHIVE_ENDPOINT}/archive/stems/download/${jobId}`) - onClose() - } - }, [jobStatus, onClose, jobId]) - - const handleClose = useCallback(() => { - if (jobId) { - cancelStemsArchiveJob({ jobId }) - } - onClose() - }, [onClose, jobId, cancelStemsArchiveJob]) - - const handleRetry = useCallback(() => { - downloadTrackStems() - }, [downloadTrackStems]) - - return ( - - - - - - - - - - {messages.zippingFiles(fileCount)} - - - {hasError ? ( - - {messages.tryAgain} - - } - > - {messages.error} - - ) : ( - - )} - - - - ) -} - -export const DownloadTrackArchiveModal = NiceModal.create(() => { - const modal = useModal() - const { - data: { trackId, fileCount } - } = useDownloadTrackArchiveModal() - - if (!trackId) { - console.error( - 'Unexpected missing trackId when rendering DownloadTrackArchiveModal' - ) - return null - } - - return ( - modal.hide()} - onClosed={() => modal.remove()} - /> - ) -}) diff --git a/packages/web/src/components/track/DownloadSection.tsx b/packages/web/src/components/track/DownloadSection.tsx index 1de0bd1642c..62722991643 100644 --- a/packages/web/src/components/track/DownloadSection.tsx +++ b/packages/web/src/components/track/DownloadSection.tsx @@ -15,8 +15,7 @@ import { usePremiumContentPurchaseModal, useWaitForDownloadModal, toastActions, - PurchaseableContentType, - useDownloadTrackArchiveModal + PurchaseableContentType } from '@audius/common/store' import { USDC } from '@audius/fixed-decimal' import { @@ -34,6 +33,7 @@ import { useDispatch } from 'react-redux' import { useModalState } from 'common/hooks/useModalState' import { Expandable } from 'components/expandable/Expandable' +import { useDownloadTrackStemsZip } from 'hooks/useDownloadTrackStemsZip' import { useIsMobile } from 'hooks/useIsMobile' import { useRequiresAccountCallback, @@ -103,8 +103,7 @@ export const DownloadSection = ({ trackId }: DownloadSectionProps) => { const { onOpen: openPremiumContentPurchaseModal } = usePremiumContentPurchaseModal() - const { onOpen: openDownloadTrackArchiveModal } = - useDownloadTrackArchiveModal() + const downloadStemsZip = useDownloadTrackStemsZip(trackId) const { data: fileSizes } = useFileSizes( { @@ -153,19 +152,9 @@ export const DownloadSection = ({ trackId }: DownloadSectionProps) => { const handleDownloadAll = useRequiresAccountCallback( (e: MouseEvent) => { e.stopPropagation() - // Only include parent track in count if it's downloadable - const parentTrackCount = access?.download ? 1 : 0 - openDownloadTrackArchiveModal({ - trackId, - fileCount: stemTracks.length + parentTrackCount - }) + downloadStemsZip() }, - [ - openDownloadTrackArchiveModal, - trackId, - stemTracks.length, - access?.download - ] + [downloadStemsZip] ) const hasStems = stemTracks.length > 0 || isUploadingStems diff --git a/packages/web/src/hooks/useDownloadTrackStemsZip.ts b/packages/web/src/hooks/useDownloadTrackStemsZip.ts new file mode 100644 index 00000000000..d1798dd26ed --- /dev/null +++ b/packages/web/src/hooks/useDownloadTrackStemsZip.ts @@ -0,0 +1,29 @@ +import { useCallback } from 'react' + +import { useDownloadTrackStems } from '@audius/common/api' +import { ID } from '@audius/common/models' + +// The response is an attachment, so the browser's download manager takes it +// and the page stays put. +const triggerDownload = (url: string) => { + const link = document.createElement('a') + link.href = url + link.click() + link.remove() +} + +/** + * Downloads a zip of the track's stems, plus the parent track when the user + * may download it. + */ +export const useDownloadTrackStemsZip = (trackId: ID) => { + const { mutateAsync: getStemsZipUrl } = useDownloadTrackStems({ trackId }) + + return useCallback(async () => { + try { + triggerDownload(await getStemsZipUrl()) + } catch (e) { + console.error('Failed to start stems download', e) + } + }, [getStemsZipUrl]) +} diff --git a/packages/web/src/pages/contest-page/components/ContestStemsCard.tsx b/packages/web/src/pages/contest-page/components/ContestStemsCard.tsx index ad126d29a9b..8460f99ba79 100644 --- a/packages/web/src/pages/contest-page/components/ContestStemsCard.tsx +++ b/packages/web/src/pages/contest-page/components/ContestStemsCard.tsx @@ -20,7 +20,6 @@ import { stemCategoryFriendlyNames } from '@audius/common/models' import { - useDownloadTrackArchiveModal, usePremiumContentPurchaseModal, useWaitForDownloadModal, PurchaseableContentType @@ -43,6 +42,7 @@ import { import { useLinkClickHandler } from 'react-router' import { UserLink } from 'components/link/UserLink' +import { useDownloadTrackStemsZip } from 'hooks/useDownloadTrackStemsZip' import { useRequiresAccountCallback } from 'hooks/useRequiresAccount' import { useTrackCoverArt } from 'hooks/useTrackCoverArt' @@ -96,8 +96,7 @@ export const ContestStemsCard = ({ trackId }: ContestStemsCardProps) => { const { onOpen: openPremiumContentPurchaseModal } = usePremiumContentPurchaseModal() - const { onOpen: openDownloadTrackArchiveModal } = - useDownloadTrackArchiveModal() + const downloadStemsZip = useDownloadTrackStemsZip(trackId) const { onOpen: openWaitForDownloadModal } = useWaitForDownloadModal() // Auto-follow the contest when a signed-in user kicks off a stem @@ -149,24 +148,15 @@ export const ContestStemsCard = ({ trackId }: ContestStemsCardProps) => { { enabled: stems.length > 0 || !!track?.is_downloadable } ) - // Action: Download All archive modal (parent + stems). + // Action: Download All as one zip (parent + stems). const handleDownloadAll = useRequiresAccountCallback( (e: MouseEvent) => { e.stopPropagation() if (!track) return followContestIfNeeded() - openDownloadTrackArchiveModal({ - trackId, - fileCount: stemsCount + (track.is_downloadable ? 1 : 0) - }) + downloadStemsZip() }, - [ - followContestIfNeeded, - openDownloadTrackArchiveModal, - trackId, - stemsCount, - track - ] + [followContestIfNeeded, downloadStemsZip, track] ) const handleUnlockAll = useRequiresAccountCallback( diff --git a/packages/web/src/pages/track-page/components/mobile/DownloadSection.tsx b/packages/web/src/pages/track-page/components/mobile/DownloadSection.tsx index abeeeb13730..b3e092c73d3 100644 --- a/packages/web/src/pages/track-page/components/mobile/DownloadSection.tsx +++ b/packages/web/src/pages/track-page/components/mobile/DownloadSection.tsx @@ -7,8 +7,7 @@ import { usePremiumContentPurchaseModal, useWaitForDownloadModal, toastActions, - PurchaseableContentType, - useDownloadTrackArchiveModal + PurchaseableContentType } from '@audius/common/store' import { USDC } from '@audius/fixed-decimal' import { @@ -25,6 +24,7 @@ import { useDispatch } from 'react-redux' import { useModalState } from 'common/hooks/useModalState' import { Expandable } from 'components/expandable/Expandable' +import { useDownloadTrackStemsZip } from 'hooks/useDownloadTrackStemsZip' import { useIsMobile } from 'hooks/useIsMobile' import { useRequiresAccountCallback, @@ -87,8 +87,7 @@ export const DownloadSection = ({ trackId }: DownloadSectionProps) => { const { onOpen: openPremiumContentPurchaseModal } = usePremiumContentPurchaseModal() - const { onOpen: openDownloadTrackArchiveModal } = - useDownloadTrackArchiveModal() + const downloadStemsZip = useDownloadTrackStemsZip(trackId) const { onOpen: openWaitForDownloadModal } = useWaitForDownloadModal() @@ -135,22 +134,9 @@ export const DownloadSection = ({ trackId }: DownloadSectionProps) => { dispatch(toast({ content: messages.followToDownload })) return } - // Only include parent track in count if it's downloadable - const parentTrackCount = partialTrack?.access?.download ? 1 : 0 - openDownloadTrackArchiveModal({ - trackId, - fileCount: stemTracks.length + parentTrackCount - }) + downloadStemsZip() }, - [ - isMobile, - shouldDisplayDownloadFollowGated, - openDownloadTrackArchiveModal, - trackId, - stemTracks.length, - dispatch, - partialTrack?.access?.download - ] + [isMobile, shouldDisplayDownloadFollowGated, downloadStemsZip, dispatch] ) const hasStems = stemTracks.length > 0 diff --git a/packages/web/src/services/audius-sdk/audiusSdk.ts b/packages/web/src/services/audius-sdk/audiusSdk.ts index 55c27b21690..c37dca3c800 100644 --- a/packages/web/src/services/audius-sdk/audiusSdk.ts +++ b/packages/web/src/services/audius-sdk/audiusSdk.ts @@ -2,8 +2,7 @@ import { AudiusSdkWithServices, createSdkWithServices, Configuration, - SolanaRelay, - ArchiverService + SolanaRelay } from '@audius/sdk' import { createWalletClient, custom, RpcRequestError } from 'viem' import { mainnet } from 'viem/chains' @@ -42,21 +41,6 @@ export const initSdk = async () => { }) ) - const archiverService = new ArchiverService( - new Configuration({ - basePath: '/archive', - middleware: [ - { - pre: async (context) => { - const endpoint = env.ARCHIVE_ENDPOINT - const url = `${endpoint}${context.url}` - return { url, init: context.init } - } - } - ] - }) - ) - // Set up a relay to identity for Ethereum RPC requests so that identity can // pay for gas fees on approved transactions. const audiusWalletClient = await getAudiusWalletClient() @@ -98,8 +82,7 @@ export const initSdk = async () => { services: { solanaRelay, audiusWalletClient, - ethWalletClient, - archiverService + ethWalletClient } }) console.debug('[audiusSdk] SDK initted.') diff --git a/packages/web/src/services/env/env.dev.ts b/packages/web/src/services/env/env.dev.ts index b8b9925965f..d0bc49444cd 100644 --- a/packages/web/src/services/env/env.dev.ts +++ b/packages/web/src/services/env/env.dev.ts @@ -7,7 +7,6 @@ export const env: Env = { API_KEY: '2dc52ec9a4c31790cab6653de0c637f680faa993', API_URL: process.env.VITE_API_URL ?? 'http://audius-api', APP_NAME: 'audius-client', - ARCHIVE_ENDPOINT: process.env.VITE_API_URL ?? 'http://audius-api', AUDIUS_NETWORK_CHAIN_ID: 1337, AUDIUS_URL: 'https://audius.co', BITSKI_CALLBACK_URL: 'https://audius.co/bitski-callback.html', diff --git a/packages/web/src/services/env/env.prod.ts b/packages/web/src/services/env/env.prod.ts index 1d7f776061d..64bb47f4bc5 100644 --- a/packages/web/src/services/env/env.prod.ts +++ b/packages/web/src/services/env/env.prod.ts @@ -7,7 +7,6 @@ export const env: Env = { API_KEY: '8acf5eb7436ea403ee536a7334faa5e9ada4b50f', API_URL: process.env.VITE_API_URL ?? 'https://api.audius.co', APP_NAME: 'audius-client', - ARCHIVE_ENDPOINT: 'https://api.audius.co', AUDIUS_NETWORK_CHAIN_ID: 31524, AUDIUS_URL: 'https://audius.co', BITSKI_CALLBACK_URL: 'https://audius.co/bitski-callback.html',