feat: download playbacks

This commit is contained in:
2026-02-12 12:34:55 +01:00
parent 13e6a71d2f
commit 8e2643b856
+157 -99
View File
@@ -1,17 +1,15 @@
import React, { useEffect, useMemo, useRef, useState } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Image, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native' import { Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
import { MaterialCommunityIcons } from '@expo/vector-icons' import { MaterialCommunityIcons } from '@expo/vector-icons'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js' import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import * as FileSystem from 'expo-file-system' import * as FileSystem from 'expo-file-system'
import { shareAsync } from 'expo-sharing' import { shareAsync } from 'expo-sharing'
import { background } from '../../assets'
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo' import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
import BorderGradientButton from '../../components/BorderGradientButton' import BorderGradientButton from '../../components/BorderGradientButton'
import MusicLandHeader from '../../components/MusicLandHeader' import MusicLandHeader from '../../components/MusicLandHeader'
import AppCheckbox from '../../components/AppCheckbox' import AppCheckbox from '../../components/AppCheckbox'
import firebase, { projectsRef, serverTimestamp, videosRef } from '../../config/firebase' import firebase, { projectsRef, serverTimestamp } from '../../config/firebase'
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase' import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import Page from '../../layouts/Page' import Page from '../../layouts/Page'
import { Routes } from '../../navigation' import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService' import { goBack, navigate } from '../../navigation/NavigationService'
@@ -137,8 +135,9 @@ const uploadSourceRecording = async ({ uri, uid, projectId }) => {
const PlaybackDownload = ({ route }) => { const PlaybackDownload = ({ route }) => {
const { currentUID } = useUserData() const { currentUID } = useUserData()
const { hasActiveSubscription } = useUser() || {} const { hasActiveSubscription, videos } = useUser() || {}
const { action, uri, project } = route.params || {} const { action: routeAction, uri, project } = route.params || {}
const action = routeAction || 'playback'
console.log('[PlaybackDownload] route params', { console.log('[PlaybackDownload] route params', {
action, action,
projectId: project?.id, projectId: project?.id,
@@ -150,18 +149,14 @@ const PlaybackDownload = ({ route }) => {
const [showConfirmModal, setShowConfirmModal] = useState(false) const [showConfirmModal, setShowConfirmModal] = useState(false)
const [pendingPlaybackUrl, setPendingPlaybackUrl] = useState(project?.playbackUrl || null) const [pendingPlaybackUrl, setPendingPlaybackUrl] = useState(project?.playbackUrl || null)
const [isPublishing, setIsPublishing] = useState(false) const [isPublishing, setIsPublishing] = useState(false)
const [isDownloading, setIsDownloading] = useState(false)
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true) const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
const hasShownAfterPlaybackRef = useRef(false) const hasShownAfterPlaybackRef = useRef(false)
const { data: video } = useDataFromRef({
ref: videosRef.doc('fr'),
simpleRef: true,
})
const afterPlaybackUrl = useMemo(() => { const afterPlaybackUrl = useMemo(() => {
if (!video) return null if (!videos) return null
return video?.afterPlayback return videos?.afterPlayback || null
}, [video]) }, [videos])
useEffect(() => { useEffect(() => {
if (action !== 'playback') return if (action !== 'playback') return
@@ -179,100 +174,152 @@ const PlaybackDownload = ({ route }) => {
const resolveAudioUrl = useCallback(() => {
if (typeof project?.songUrl === 'string' && project.songUrl.trim()) {
return project.songUrl.trim()
}
const urls = Array.isArray(project?.musicUrls) ? project.musicUrls : []
if (!urls.length) return null
const idx = Number.isFinite(Number(project?.songIndex)) ? Number(project.songIndex) : 0
const candidate = urls[idx]
if (typeof candidate === 'string' && candidate.trim()) {
return candidate.trim()
}
return null
}, [project])
const handleDownloadUri = async () => { const handleDownloadUri = async () => {
if (isPublishing) return if (isPublishing || isDownloading) return
if (action === 'playback' && project?.id) { if (action !== 'playback') {
if (pendingPlaybackUrl) { setTooltip({
await triggerDownload(pendingPlaybackUrl, project?.title) type: 'error',
return text: 'Téléchargement indisponible pour cette action',
}
// Publication du playback
console.log('[PlaybackDownload] handleDownloadUri playback', {
projectId: project.id,
uri,
currentUID,
}) })
const audioUrl = project?.songUrl || null return
if (!audioUrl) { }
setTooltip({
type: 'error', const playbackUrlToDownload = pendingPlaybackUrl || project?.playbackUrl || null
text: 'Aucune piste audio disponible pour ce projet', if (playbackUrlToDownload) {
}) setIsDownloading(true)
return
}
let tempSourcePath = null
try { try {
setIsLoading(true) await triggerDownload(playbackUrlToDownload, project?.title)
const { sourcePath, videoUrl } = await uploadSourceRecording({ } finally {
uri, setIsDownloading(false)
uid: currentUID, }
projectId: project.id, return
}) }
tempSourcePath = sourcePath
if (!project?.id) {
const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio') setTooltip({
type: 'error',
const payload = { text: 'Projet introuvable pour ce playback',
projectId: project?.id, })
videoUrl, return
audioUrl, }
storagePath: `users/${currentUID}/projects/${project.id}/playback.mp4`,
} if (!uri) {
setTooltip({
console.log('[PlaybackDownload] calling upload-mergeVideoAndAudio', payload) type: 'error',
text: 'Aucune vidéo trouvée pour ce playback',
const { data: result } = await callable(payload) })
return
console.log('[PlaybackDownload] upload-mergeVideoAndAudio result', result) }
const resultURI = result?.url || null if (!currentUID) {
setTooltip({
if (resultURI) { type: 'error',
setPendingPlaybackUrl(resultURI) text: 'Utilisateur non authentifié',
setTooltip({ })
type: 'success', return
text: 'Playback prêt à télécharger', }
})
await triggerDownload(resultURI, project?.title) const audioUrl = resolveAudioUrl()
if (tempSourcePath) { if (!audioUrl) {
try { setTooltip({
await firebase.storage().ref(tempSourcePath).delete() type: 'error',
} catch (cleanupError) { text: 'Aucune piste audio disponible pour ce projet',
console.log('[PlaybackDownload] unable to delete temp source', { })
message: cleanupError?.message, return
code: cleanupError?.code, }
})
} // Publication du playback
} console.log('[PlaybackDownload] handleDownloadUri playback', {
} else { projectId: project.id,
setTooltip({ uri,
type: 'error', currentUID,
text: 'Erreur lors de la publication du playback', })
})
} let tempSourcePath = null
} catch (error) { try {
console.log('[PlaybackDownload] error upload playback', { setIsDownloading(true)
message: error?.message, setIsLoading(true)
code: error?.code, const { sourcePath, videoUrl } = await uploadSourceRecording({
name: error?.name, uri,
details: error?.details, uid: currentUID,
projectId: project.id,
})
tempSourcePath = sourcePath
const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio')
const payload = {
projectId: project?.id,
videoUrl,
audioUrl,
storagePath: `users/${currentUID}/projects/${project.id}/playback.mp4`,
}
console.log('[PlaybackDownload] calling upload-mergeVideoAndAudio', payload)
const { data: result } = await callable(payload)
console.log('[PlaybackDownload] upload-mergeVideoAndAudio result', result)
const resultURI = result?.url || null
if (resultURI) {
setPendingPlaybackUrl(resultURI)
setTooltip({
type: 'success',
text: 'Playback prêt à télécharger',
}) })
await triggerDownload(resultURI, project?.title)
if (tempSourcePath) { if (tempSourcePath) {
try { try {
await firebase.storage().ref(tempSourcePath).delete() await firebase.storage().ref(tempSourcePath).delete()
} catch { } } catch (cleanupError) {
console.log('[PlaybackDownload] unable to delete temp source', {
message: cleanupError?.message,
code: cleanupError?.code,
})
}
} }
} else {
setTooltip({ setTooltip({
type: 'error', type: 'error',
text: String(error?.message || 'Erreur lors de la publication du playback'), text: 'Erreur lors de la publication du playback',
}) })
} finally {
releaseBlobUrl(uri || null)
setIsLoading(false)
} }
// Handle playback download } catch (error) {
} else { console.log('[PlaybackDownload] error upload playback', {
// Handle song download message: error?.message,
code: error?.code,
name: error?.name,
details: error?.details,
})
if (tempSourcePath) {
try {
await firebase.storage().ref(tempSourcePath).delete()
} catch { }
}
setTooltip({
type: 'error',
text: String(error?.message || 'Erreur lors de la publication du playback'),
})
} finally {
releaseBlobUrl(uri || null)
setIsDownloading(false)
setIsLoading(false)
} }
} }
@@ -299,7 +346,7 @@ const PlaybackDownload = ({ route }) => {
} }
let tempSourcePath = null let tempSourcePath = null
let playbackUrlToSave = pendingPlaybackUrl || project?.playbackUrl || null let playbackUrlToSave = pendingPlaybackUrl || project?.playbackUrl || null
const audioUrl = project?.songUrl || null const audioUrl = resolveAudioUrl()
if (!audioUrl && !playbackUrlToSave) { if (!audioUrl && !playbackUrlToSave) {
setTooltip({ setTooltip({
type: 'error', type: 'error',
@@ -307,6 +354,13 @@ const PlaybackDownload = ({ route }) => {
}) })
return return
} }
if (!currentUID) {
setTooltip({
type: 'error',
text: 'Utilisateur non authentifié',
})
return
}
try { try {
setIsPublishing(true) setIsPublishing(true)
@@ -417,7 +471,11 @@ const PlaybackDownload = ({ route }) => {
</View> </View>
)} )}
<Pressable style={styles.downloadTile} onPress={handleDownloadUri}> <Pressable
style={[styles.downloadTile, (isPublishing || isDownloading) && { opacity: 0.6 }]}
onPress={handleDownloadUri}
disabled={isPublishing || isDownloading}
>
<MaterialCommunityIcons name="download" size={22} color={Palette.white} /> <MaterialCommunityIcons name="download" size={22} color={Palette.white} />
<Text style={styles.downloadText}>Télécharger le playback</Text> <Text style={styles.downloadText}>Télécharger le playback</Text>
</Pressable> </Pressable>