From 63cedeb84b409f190373f8e88ef3f7f4828ad127 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Thu, 22 Sep 2022 13:03:17 -0400 Subject: [PATCH 01/10] download cover art success --- .../src/services/track-downloader/index.ts | 1 + .../track-downloader/track-downloader.ts | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 packages/mobile/src/services/track-downloader/index.ts create mode 100644 packages/mobile/src/services/track-downloader/track-downloader.ts diff --git a/packages/mobile/src/services/track-downloader/index.ts b/packages/mobile/src/services/track-downloader/index.ts new file mode 100644 index 0000000000..a43f998670 --- /dev/null +++ b/packages/mobile/src/services/track-downloader/index.ts @@ -0,0 +1 @@ +export * from './track-downloader' diff --git a/packages/mobile/src/services/track-downloader/track-downloader.ts b/packages/mobile/src/services/track-downloader/track-downloader.ts new file mode 100644 index 0000000000..95dce8b5ad --- /dev/null +++ b/packages/mobile/src/services/track-downloader/track-downloader.ts @@ -0,0 +1,32 @@ +import { cacheTracksSelectors } from '@audius/common' +import RNFS from 'react-native-fs' + +import { store } from 'app/store' +const { getTracks, getTrack } = cacheTracksSelectors + +// import { apiClient } from '../audius-api-client' + +export const downloadAnyOldTrack = () => { + const tracks = getTracks(store.getState(), {}) + const firstTrackId = Object.keys(tracks).pop() + return downloadTrack(firstTrackId) +} + +export const downloadTrack = async (trackId) => { + const track = getTrack(store.getState(), { id: trackId }) + const coverArtUri = track?._cover_art_sizes?.['150x150'] + const destinationPath = `${ + RNFS.CachesDirectoryPath + }/${coverArtUri?.replaceAll('/', '_')}` + console.log(coverArtUri) + console.log(destinationPath) + const downloadAction = coverArtUri + ? RNFS.downloadFile({ + fromUrl: coverArtUri, + toFile: destinationPath + }) + : null + const result = await downloadAction?.promise + console.log(result) + return result?.statusCode ?? null +} From 05e325225380d011548b3c3c7a97f9f935ab93e8 Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Fri, 23 Sep 2022 12:30:56 -0400 Subject: [PATCH 02/10] track and cover art downloads working --- .../track-downloader/track-downloader.ts | 106 ++++++++++++++---- 1 file changed, 87 insertions(+), 19 deletions(-) diff --git a/packages/mobile/src/services/track-downloader/track-downloader.ts b/packages/mobile/src/services/track-downloader/track-downloader.ts index 95dce8b5ad..b3ab1cb08c 100644 --- a/packages/mobile/src/services/track-downloader/track-downloader.ts +++ b/packages/mobile/src/services/track-downloader/track-downloader.ts @@ -1,32 +1,100 @@ -import { cacheTracksSelectors } from '@audius/common' -import RNFS from 'react-native-fs' +import type { Track, UserMetadata } from '@audius/common' +import { + encodeHashId, + accountSelectors, + cacheTracksSelectors +} from '@audius/common' +import RNFS, { exists } from 'react-native-fs' import { store } from 'app/store' + +import { apiClient } from '../audius-api-client' +const { getUserId } = accountSelectors const { getTracks, getTrack } = cacheTracksSelectors -// import { apiClient } from '../audius-api-client' +// TODO: make this CachesDirectoryPath, but Downloads is easier to test with +// export const downloadsRoot = RNFS.CachesDirectoryPath +export const downloadsRoot = RNFS.CachesDirectoryPath export const downloadAnyOldTrack = () => { const tracks = getTracks(store.getState(), {}) - const firstTrackId = Object.keys(tracks).pop() + const firstTrackId = Object.values(tracks)[1]?.track_id + if (!firstTrackId) return return downloadTrack(firstTrackId) } -export const downloadTrack = async (trackId) => { - const track = getTrack(store.getState(), { id: trackId }) - const coverArtUri = track?._cover_art_sizes?.['150x150'] - const destinationPath = `${ - RNFS.CachesDirectoryPath - }/${coverArtUri?.replaceAll('/', '_')}` - console.log(coverArtUri) - console.log(destinationPath) - const downloadAction = coverArtUri - ? RNFS.downloadFile({ - fromUrl: coverArtUri, - toFile: destinationPath - }) - : null - const result = await downloadAction?.promise +export const downloadTrack = async (trackId: number) => { + const state = store.getState() + const track = getTrack(state, { id: trackId }) + if (!track) return false + + const coverArtUri = getCoverArtUri(track) + const [coverArtDirectory, coverArtFileName] = getCoverArtDestination(track) + if (coverArtUri && coverArtDirectory && coverArtFileName) { + await downloadIfNotExists(coverArtUri, coverArtDirectory, coverArtFileName) + } + await tryDownloadTrackFromEachCreatorNode(track) +} + +const tryDownloadTrackFromEachCreatorNode = async (track: Track) => { + const state = store.getState() + const user = ( + await apiClient.getUser({ + userId: track?.owner_id, + currentUserId: getUserId(state as any) // todo + }) + )[0] as UserMetadata + console.log(user) + const encodedTrackId = encodeHashId(track.track_id) + const creatorNodeEndpoints = user.creator_node_endpoint.split(',') + for (const creatorNodeEndpoint of creatorNodeEndpoints) { + const uri = `${creatorNodeEndpoint}/tracks/stream/${encodedTrackId}` + const [audioDirectory, audioFileName] = getAudioDestination(track) + await downloadIfNotExists(uri, audioDirectory, audioFileName) + } +} + +const getCoverArtUri = (track: Track) => { + return track._cover_art_sizes?.['150x150'] +} + +const getCoverArtDestination = (track: Track) => { + const uri = getCoverArtUri(track) + if (!uri) return [null, null] + const fileName = getFileNameFromUri(uri) + return [`${downloadsRoot}/tracks/${track.track_id}`, fileName] +} + +// TODO: why isn't route id on the type, even though it's populated in state +const getAudioDestination = (track) => { + const fileName = `${track.route_id.replaceAll('/', '_')}.mp3` + return [`${downloadsRoot}/tracks/${track.track_id}`, fileName] +} + +const getFileNameFromUri = (uri: string) => { + return uri.split('/').slice(-1)[0] +} + +const downloadIfNotExists = async ( + uri: string, + destinationDirectory: string, + fileName: string, + overwrite?: boolean +) => { + if (!uri) return null + const fullFilePath = `${destinationDirectory}/${fileName}` + if (!overwrite && (await exists(fullFilePath))) { + console.log(`file at ${fullFilePath} exists`) + return null + } + + await RNFS.mkdir(destinationDirectory) + + const result = await RNFS.downloadFile({ + fromUrl: uri, + toFile: fullFilePath + })?.promise + console.log(result) return result?.statusCode ?? null } From 770d75203075bf5d51a7e373ec6402f67d1e165b Mon Sep 17 00:00:00 2001 From: amendelsohn Date: Fri, 23 Sep 2022 17:24:37 -0400 Subject: [PATCH 03/10] hacked together offline playback works --- .../mobile/src/components/audio/Audio.tsx | 27 +++++++-- packages/mobile/src/hooks/useOfflineTrack.ts | 23 ++++++++ .../favorites-screen/FavoritesScreen.tsx | 6 +- .../screens/favorites-screen/TracksTab.tsx | 15 ++++- .../src/screens/feed-screen/FeedScreen.tsx | 8 ++- .../track-downloader/track-downloader.ts | 55 +++++++++++++++---- packages/mobile/src/utils/fileSystem.ts | 3 + 7 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 packages/mobile/src/hooks/useOfflineTrack.ts create mode 100644 packages/mobile/src/utils/fileSystem.ts diff --git a/packages/mobile/src/components/audio/Audio.tsx b/packages/mobile/src/components/audio/Audio.tsx index ccab748c7f..f266fb9fae 100644 --- a/packages/mobile/src/components/audio/Audio.tsx +++ b/packages/mobile/src/components/audio/Audio.tsx @@ -18,6 +18,7 @@ import type { OnProgressData } from 'react-native-video' import Video from 'react-native-video' import { useDispatch, useSelector } from 'react-redux' +import { useOfflineTrackUri } from 'app/hooks/useOfflineTrack' import { audiusBackendInstance } from 'app/services/audius-backend-instance' import { useChromecast } from './GoogleCast' @@ -331,6 +332,8 @@ export const Audio = () => { ] ) + const offlineSrc = useOfflineTrackUri(track) + if (!track || track.is_delete) return null const gateways = trackOwner @@ -344,15 +347,27 @@ export const Audio = () => { gateways }) + const source = offlineSrc + ? { + uri: offlineSrc + } + : m3u8 + ? { + uri: m3u8, + type: 'm3u8' + } + : null + + if (offlineSrc) { + console.log(`using offline src [${offlineSrc}]`) + } + return ( - {m3u8 && ( + {source && (