diff --git a/functions/src/music.js b/functions/src/music.js index 54356c9..667deff 100644 --- a/functions/src/music.js +++ b/functions/src/music.js @@ -1,4 +1,4 @@ -const { onCall, onRequest } = require("firebase-functions/v2/https"); +const { onCall, onRequest, HttpsError } = require("firebase-functions/v2/https"); const axios = require("axios"); const admin = require("firebase-admin"); const { logger } = require("firebase-functions/logger"); @@ -14,6 +14,46 @@ const { // Initialiser Firestorey const db = admin.firestore(); +/** + * Marque un projet comme échoué suite à une erreur Suno + * @param {string} projectId - Identifiant du projet + * @param {Object} error - Erreur capturée (axios) + */ +async function markProjectMusicFailure(projectId, error) { + if (!projectId) return; + try { + const docRef = db.collection("projects").doc(projectId); + const status = error?.response?.status || error?.status || null; + const sunoMessage = + error?.response?.data?.msg || + error?.response?.data?.message || + error?.message || + "Erreur lors de la génération de musique"; + const errorPayload = { + source: "SUNO_API", + message: sunoMessage, + }; + if (status) errorPayload.status = status; + if (error?.code) errorPayload.code = error.code; + + await docRef.set( + { + musicStatus: "FAILED", + sunoTaskId: admin.firestore.FieldValue.delete(), + generationStartAt: admin.firestore.FieldValue.delete(), + musicError: errorPayload, + updatedAt: admin.firestore.FieldValue.serverTimestamp(), + }, + { merge: true } + ); + } catch (err) { + console.error( + "❌ [generateMusic] Impossible de marquer le projet en erreur:", + err + ); + } +} + /** * Limite la longueur d'une chaîne * @param {string} str - La chaîne à limiter @@ -40,7 +80,7 @@ function extractVoiceStrings(voice) { } if (typeof voice === "object") { return Object.values(voice).filter( - (v) => typeof v === "string" && v.trim(), + (v) => typeof v === "string" && v.trim() ); } return []; @@ -94,7 +134,7 @@ function detectVocalGender(voiceInput = "") { if (Array.isArray(voiceInput)) { const baseItem = voiceInput.find( (v) => - v && (v.category === "base" || v?.category?.toLowerCase() === "base"), + v && (v.category === "base" || v?.category?.toLowerCase() === "base") ); const text = baseItem ? baseItem.value || baseItem.text || baseItem : null; if (text) return detectVocalGender(text); @@ -306,7 +346,7 @@ exports.generateMusic = onCall(async ({ data = {} }) => { "Content-Type": "application/json", Authorization: `Bearer ${SUNO_API_KEY}`, }, - }, + } ); const parsed = response.data; @@ -325,9 +365,26 @@ exports.generateMusic = onCall(async ({ data = {} }) => { }; } catch (error) { console.error("❌ Erreur lors de la génération de musique:", error); - throw new Error( - `Erreur lors de la génération de musique: ${error.message}`, - ); + try { + await markProjectMusicFailure(data?.projectId, error); + } catch (markErr) { + console.error( + "❌ Erreur lors de la mise à jour du statut de projet:", + markErr + ); + } + + const status = error?.response?.status || "INTERNAL_ERROR"; + const message = + error?.response?.data?.msg || + error?.response?.data?.message || + error?.message || + "Erreur lors de la génération de musique avec Suno"; + + throw new HttpsError("internal", message, { + status, + source: "SUNO_API", + }); } }); @@ -355,7 +412,7 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => { Authorization: `Bearer ${SUNO_API_KEY}`, }, timeout: 30000, // 30 secondes de timeout - }, + } ); parsed = response.data; @@ -510,7 +567,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { const audioUrls = tracks .map( (t) => - t.audio_url || t.audioUrl || t.stream_audio_url || t.streamAudioUrl, + t.audio_url || t.audioUrl || t.stream_audio_url || t.streamAudioUrl ) .filter(Boolean) .slice(0, 2); @@ -525,7 +582,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { has_audio_url: !!t.audio_url, has_stream_audio_url: !!t.stream_audio_url, })), - }, + } ); } @@ -550,14 +607,14 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { }, }); const downloadUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent( - path, + path )}?alt=media&token=${token}`; console.log("✅ [SunoCallback] Sauvegardé:", path, "URL:", downloadUrl); return { path, url: downloadUrl }; } catch (e) { console.error( `❌ [SunoCallback] Échec save piste ${index + 1}:`, - e.message, + e.message ); return null; } @@ -575,9 +632,10 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { { musicStatus: "GENERATED", musicUrls, + musicError: admin.firestore.FieldValue.delete(), updatedAt: admin.firestore.FieldValue.serverTimestamp(), }, - { merge: true }, + { merge: true } ); console.log("🏷️ [SunoCallback] Projet marqué GENERATED", { projectId, diff --git a/src/screens/Studio/CreatingSong.js b/src/screens/Studio/CreatingSong.js index 176a2e0..e08ecfc 100644 --- a/src/screens/Studio/CreatingSong.js +++ b/src/screens/Studio/CreatingSong.js @@ -1,18 +1,19 @@ -import { View, Text, Image, Pressable, Platform } from "react-native"; -import React, { useEffect, useState } from "react"; -import { ai, icons } from "../../assets"; import { BlurView } from "expo-blur"; -import Style, { size } from "../../styles/Style"; -import { Palette } from "../../styles"; -import { FONT_FAMILY } from "../../styles/Fonts"; +import moment from "moment"; +import React, { useEffect, useState } from "react"; +import { Image, Platform, Pressable, Text, View } from "react-native"; +import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; +import { ai, icons } from "../../assets"; +import AppAlert from "../../components/Alert"; +import GradientButton from "../../components/GradientButton"; import ProgressBar from "../../components/ProgressBar"; +import firebase, { projectsRef } from "../../config/firebase"; +import { Routes } from "../../navigation"; import { goBack, navigate } from "../../navigation/NavigationService"; import { useUser } from "../../providers/UserDataProvider"; -import { Routes } from "../../navigation"; -import GradientButton from "../../components/GradientButton"; -import firebase, { projectsRef } from "../../config/firebase"; -import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; -import moment from "moment"; +import { Palette } from "../../styles"; +import { FONT_FAMILY } from "../../styles/Fonts"; +import Style, { size } from "../../styles/Style"; const CreatingSong = ({ active, config }) => { const [progress, setProgress] = useState(0); @@ -118,6 +119,7 @@ const CreatingSong = ({ active, config }) => { { sunoTaskId: taskId, musicStatus: "GENERATING", + musicError: firebase.firestore.FieldValue.delete(), musicConfig: { title: config?.title || "", lyrics: config?.lyrics || [], @@ -134,7 +136,40 @@ const CreatingSong = ({ active, config }) => { ); } } catch (e) { - console.log(e); + console.log("CreatingSong generation error", e?.message); + const rawMessage = e?.message || ""; + const sanitized = + typeof rawMessage === "string" + ? rawMessage.replace(/^INTERNAL:/i, "").trim() + : ""; + const fallbackMessage = + e?.details?.status || + "Le service est actuellement indisponible. Merci de réessayer."; + AppAlert( + "Impossible de générer la musique veuillez réessayer plus tard." + ); + if (config?.projectId) { + try { + await projectsRef.doc(config.projectId).set( + { + musicStatus: "FAILED", + sunoTaskId: firebase.firestore.FieldValue.delete(), + generationStartAt: firebase.firestore.FieldValue.delete(), + updatedAt: firebase.firestore.FieldValue.serverTimestamp(), + musicError: { + source: "SUNO_API", + message: sanitized || fallbackMessage, + }, + }, + { merge: true } + ); + } catch (firestoreError) { + console.log( + "CreatingSong Firestore update error", + firestoreError?.message + ); + } + } } finally { await setIsLoading(false); } diff --git a/src/screens/Studio/GeneratingSong.js b/src/screens/Studio/GeneratingSong.js index 2bc1d0e..c3343c2 100644 --- a/src/screens/Studio/GeneratingSong.js +++ b/src/screens/Studio/GeneratingSong.js @@ -123,6 +123,7 @@ const GeneratingSong = () => { voice: cfg?.voice, instruments: cfg?.instruments, tempo: cfg?.tempo, + projectId: selectedProjectId || selectedProject?.id || null, }); const taskId = data?.response?.data?.taskId || data?.response?.data?.task_id; @@ -130,6 +131,7 @@ const GeneratingSong = () => { const baseUpdate = { sunoTaskId: taskId, musicStatus: "GENERATING", + musicError: firebase.firestore.FieldValue.delete(), generationStartAt: firebase.firestore.FieldValue.serverTimestamp(), updatedAt: firebase.firestore.FieldValue.serverTimestamp(), }; @@ -152,6 +154,42 @@ const GeneratingSong = () => { } } catch (e) { console.log("GeneratingSong error", e?.message); + const rawMessage = e?.message || ""; + const sanitized = + typeof rawMessage === "string" + ? rawMessage.replace(/^INTERNAL:/i, "").trim() + : ""; + const fallbackMessage = + e?.details?.message || + e?.details?.status || + "Le service Suno est indisponible. Veuillez réessayer plus tard."; + AppAlert( + "Impossible de générer la musique", + sanitized || fallbackMessage + ); + if (selectedProjectId) { + try { + await projectsRef.doc(selectedProjectId).set( + { + musicStatus: "FAILED", + sunoTaskId: firebase.firestore.FieldValue.delete(), + generationStartAt: firebase.firestore.FieldValue.delete(), + updatedAt: firebase.firestore.FieldValue.serverTimestamp(), + musicError: { + source: "SUNO_API", + message: sanitized || fallbackMessage, + }, + }, + { merge: true } + ); + } catch (firestoreError) { + console.log( + "GeneratingSong Firestore update error", + firestoreError?.message + ); + } + } + askedRef.current = false; } finally { await setIsLoading(false); } @@ -181,7 +219,8 @@ const GeneratingSong = () => { status == null || status === "" || status === "PENDING" || - status === "READY"; + status === "READY" || + status === "FAILED"; if (titleOk && isIdle && !askedRef.current) { askedRef.current = true;