more fixes and changes, crash, design …
This commit is contained in:
+223
-205
@@ -18,8 +18,6 @@ const {
|
||||
SUNO_STATUS_PATH,
|
||||
} = require("../config/suno");
|
||||
|
||||
const MAX_STORED_MUSIC_TRACKS = 4;
|
||||
|
||||
/**
|
||||
* Marque un projet comme échoué suite à une erreur Suno
|
||||
* @param {string} projectId - Identifiant du projet
|
||||
@@ -55,15 +53,9 @@ async function markProjectMusicFailure(projectId, error) {
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
const receiverId =
|
||||
typeof projectData?.userId === "string" && projectData.userId.trim()
|
||||
? projectData.userId.trim()
|
||||
: null;
|
||||
const receiverId = sanitizeField(projectData?.userId);
|
||||
if (receiverId) {
|
||||
const projectTitle =
|
||||
typeof projectData?.title === "string" && projectData.title.trim()
|
||||
? projectData.title.trim()
|
||||
: "ton projet";
|
||||
const projectTitle = sanitizeField(projectData?.title, "ton projet");
|
||||
const message = `La génération de musique pour "${projectTitle}" a échoué.`;
|
||||
try {
|
||||
await sendNotification({
|
||||
@@ -205,6 +197,161 @@ function detectVocalGender(voiceInput = "") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sanitizeField = (value, fallback = null) => {
|
||||
const cleaned = typeof value === "string" ? value.trim() : value;
|
||||
if (typeof cleaned !== "string" || !cleaned) return fallback;
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
const sanitizeMusicUrls = (urls = []) =>
|
||||
(Array.isArray(urls) ? urls : [])
|
||||
.filter((url) => typeof url === "string" && url.trim())
|
||||
.map((url) => url.trim());
|
||||
|
||||
const formatProjectMeta = (projectData = {}) => {
|
||||
const userId = sanitizeField(projectData?.userId);
|
||||
const projectTitle = sanitizeField(projectData?.title, "ton projet");
|
||||
return { userId, projectTitle };
|
||||
};
|
||||
|
||||
const parseSunoCallbackPayload = (rawBody = {}) => {
|
||||
const body = rawBody || {};
|
||||
const code = body.code ?? body.statusCode ?? null;
|
||||
const callbackType = (body?.data?.callbackType || "")
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
const status = (body.status || body.state || callbackType || "")
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
const taskId = sanitizeField(body?.data?.task_id);
|
||||
const tracks = Array.isArray(body?.data?.data)
|
||||
? body.data.data
|
||||
: Array.isArray(body.data)
|
||||
? body.data
|
||||
: [];
|
||||
|
||||
return { code, status, taskId, tracks };
|
||||
};
|
||||
|
||||
const extractAudioUrlsFromTracks = (tracks = []) =>
|
||||
tracks
|
||||
.map(
|
||||
(t) =>
|
||||
t.audio_url || t.audioUrl || t.stream_audio_url || t.streamAudioUrl,
|
||||
)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2);
|
||||
|
||||
const fetchProjectByTaskId = async (taskId) => {
|
||||
const snapshot = await refList.projects
|
||||
.where("sunoTaskId", "==", taskId)
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (snapshot.empty) {
|
||||
throw new Error("PROJECT_NOT_FOUND_FOR_TASK");
|
||||
}
|
||||
|
||||
const doc = snapshot.docs[0];
|
||||
return {
|
||||
projectId: doc.id,
|
||||
projectData: doc.data() || {},
|
||||
projectRef: doc.ref,
|
||||
};
|
||||
};
|
||||
|
||||
const downloadTrackToStorage = async (
|
||||
url,
|
||||
{ userId, projectId, taskId, bucket, index },
|
||||
) => {
|
||||
if (!url) return null;
|
||||
try {
|
||||
console.log(`⬇️ [SunoCallback] Téléchargement piste ${index + 1}`);
|
||||
const resp = await axios.get(url, { responseType: "arraybuffer" });
|
||||
const buffer = Buffer.from(resp.data);
|
||||
const path = `users/${userId}/projects/${projectId}/task-${taskId}-${index + 1}.mp3`;
|
||||
const token = require("crypto").randomUUID();
|
||||
const file = bucket.file(path);
|
||||
await file.save(buffer, {
|
||||
resumable: false,
|
||||
metadata: {
|
||||
contentType: "audio/mpeg",
|
||||
cacheControl: "public, max-age=31536000",
|
||||
metadata: { firebaseStorageDownloadTokens: token },
|
||||
},
|
||||
});
|
||||
const downloadUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
|
||||
path,
|
||||
)}?alt=media&token=${token}`;
|
||||
console.log("✅ [SunoCallback] Sauvegardé:", path, "URL:", downloadUrl);
|
||||
return { path, url: downloadUrl };
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ [SunoCallback] Échec save piste ${index + 1}:`,
|
||||
error.message,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const saveTracksToStorage = async (audioUrls, meta) => {
|
||||
if (!audioUrls?.length) return [];
|
||||
const bucket = admin.storage().bucket();
|
||||
console.log("🪣 [SunoCallback] Bucket:", bucket.name);
|
||||
const context = { ...meta, bucket };
|
||||
const results = await Promise.all(
|
||||
audioUrls.map((url, index) =>
|
||||
downloadTrackToStorage(url, { ...context, index }),
|
||||
),
|
||||
);
|
||||
return results.filter(Boolean).map((entry) => entry.url);
|
||||
};
|
||||
|
||||
const mergeMusicUrls = async (projectRef, newUrls) => {
|
||||
const sanitizedNewUrls = sanitizeMusicUrls(newUrls);
|
||||
console.log("➕ [SunoCallback] Nouveaux morceaux à ajouter:", {
|
||||
count: sanitizedNewUrls.length,
|
||||
urls: sanitizedNewUrls,
|
||||
});
|
||||
|
||||
let existingUrls = [];
|
||||
try {
|
||||
const snapshot = await projectRef.get();
|
||||
existingUrls = sanitizeMusicUrls(snapshot?.data()?.musicUrls);
|
||||
console.log("📦 [SunoCallback] Morceaux déjà stockés:", {
|
||||
count: existingUrls.length,
|
||||
urls: existingUrls,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"⚠️ [SunoCallback] Impossible de récupérer les anciennes musiques:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
const allUrls = [...existingUrls, ...sanitizedNewUrls];
|
||||
const dedupedUrls = allUrls.filter(
|
||||
(url, index) => allUrls.indexOf(url) === index,
|
||||
);
|
||||
|
||||
console.log("🎶 [SunoCallback] Morceaux conservés après fusion:", {
|
||||
count: dedupedUrls.length,
|
||||
urls: dedupedUrls,
|
||||
});
|
||||
|
||||
await projectRef.set(
|
||||
{
|
||||
musicStatus: "GENERATED",
|
||||
musicUrls: dedupedUrls,
|
||||
musicError: FieldValue.delete(),
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
return dedupedUrls;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construit le style musical à partir des paramètres
|
||||
* @param {Object} params - Les paramètres de style
|
||||
@@ -535,232 +682,103 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => {
|
||||
* de musique est terminée
|
||||
*/
|
||||
exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
|
||||
try {
|
||||
console.log("🎵 [SunoCallback] Reçu:", JSON.stringify(req.body));
|
||||
if (req.method !== "POST") {
|
||||
console.warn("⚠️ [SunoCallback] Méthode non autorisée:", req.method);
|
||||
return res.status(405).json({ error: "Méthode non autorisée" });
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
console.warn("⚠️ [SunoCallback] Méthode non autorisée:", req.method);
|
||||
return res.status(405).json({ error: "Méthode non autorisée" });
|
||||
}
|
||||
console.log("🎵 [SunoCallback] Reçu:", JSON.stringify(req.body));
|
||||
|
||||
const body = req.body || {};
|
||||
const code = body.code ?? body.statusCode ?? null;
|
||||
const callbackType = (body?.data?.callbackType || "")
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
const status = (body.status || body.state || callbackType)
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
const taskId =
|
||||
body.taskId ||
|
||||
body.task_id ||
|
||||
body?.data?.taskId ||
|
||||
body?.data?.task_id ||
|
||||
null;
|
||||
const tracks = Array.isArray(body?.data?.data)
|
||||
? body.data.data
|
||||
: Array.isArray(body.data)
|
||||
? body.data
|
||||
: [];
|
||||
const { code, status, taskId, tracks } = parseSunoCallbackPayload(req.body);
|
||||
console.log("🎯 [SunoCallback] Détails:", {
|
||||
code,
|
||||
status,
|
||||
taskId,
|
||||
count: tracks.length,
|
||||
});
|
||||
|
||||
console.log("🎯 [SunoCallback] Détails:", {
|
||||
if (code !== 200 || status !== "complete") {
|
||||
console.log("ℹ️ [SunoCallback] Callback ignoré (code/status)", {
|
||||
code,
|
||||
status,
|
||||
callbackType,
|
||||
taskId,
|
||||
count: tracks.length,
|
||||
});
|
||||
return res.status(200).json({ success: true, ignored: true });
|
||||
}
|
||||
|
||||
// On n'agit que si code === 200 et status === "complete"
|
||||
if (code !== 200 || status !== "complete") {
|
||||
console.log("ℹ️ [SunoCallback] Callback ignoré (code/status)", {
|
||||
code,
|
||||
status,
|
||||
});
|
||||
return res.status(200).json({ success: true, ignored: true });
|
||||
}
|
||||
if (!taskId) {
|
||||
console.warn("⚠️ [SunoCallback] taskId manquant dans le callback");
|
||||
return res.status(200).json({ success: true, ignored: true });
|
||||
}
|
||||
|
||||
if (!taskId) {
|
||||
console.warn("⚠️ [SunoCallback] taskId manquant dans le callback");
|
||||
return res.status(200).json({ success: true, ignored: true });
|
||||
}
|
||||
|
||||
// 1) Récupérer le projectId associé au taskId
|
||||
let projectId = null;
|
||||
const projSnap = await refList.projects
|
||||
.where("sunoTaskId", "==", taskId)
|
||||
.limit(1)
|
||||
.get();
|
||||
if (projSnap.empty) {
|
||||
throw new Error("Aucun projet trouvé pour ce taskId");
|
||||
}
|
||||
const projectDoc = projSnap.docs[0];
|
||||
const projectData = projectDoc?.data() || {};
|
||||
projectId = projectDoc.id;
|
||||
const userId =
|
||||
typeof projectData.userId === "string" && projectData.userId.trim()
|
||||
? projectData.userId.trim()
|
||||
: null;
|
||||
const projectTitle =
|
||||
typeof projectData.title === "string" && projectData.title.trim()
|
||||
? projectData.title.trim()
|
||||
: "ton projet";
|
||||
|
||||
if (!projectId) {
|
||||
console.warn("⚠️ [SunoCallback] Aucun projet trouvé pour", { taskId });
|
||||
return res.status(200).json({ success: true, ignored: true });
|
||||
}
|
||||
|
||||
// 2) Extraire jusqu'à 2 URLs audio
|
||||
const audioUrls = tracks
|
||||
.map(
|
||||
(t) =>
|
||||
t.audio_url || t.audioUrl || t.stream_audio_url || t.streamAudioUrl,
|
||||
)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2);
|
||||
try {
|
||||
const { projectId, projectData, projectRef } =
|
||||
await fetchProjectByTaskId(taskId);
|
||||
const { userId, projectTitle } = formatProjectMeta(projectData);
|
||||
|
||||
const audioUrls = extractAudioUrlsFromTracks(tracks);
|
||||
if (audioUrls.length < 2) {
|
||||
console.warn(
|
||||
"⚠️ [SunoCallback] Moins de 2 pistes audio dans le callback",
|
||||
{
|
||||
found: audioUrls.length,
|
||||
tracksSample: tracks.map((t) => ({
|
||||
id: t.id,
|
||||
has_audio_url: !!t.audio_url,
|
||||
has_stream_audio_url: !!t.stream_audio_url,
|
||||
})),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 3) Télécharger et sauvegarder dans Cloud Storage + récupérer download URLs
|
||||
const bucket = admin.storage().bucket();
|
||||
console.log("🪣 [SunoCallback] Bucket:", bucket.name);
|
||||
const saveOne = async (url, index) => {
|
||||
if (!url) return null;
|
||||
const storedUrls = await saveTracksToStorage(audioUrls, {
|
||||
userId,
|
||||
projectId,
|
||||
taskId,
|
||||
});
|
||||
|
||||
const musicUrls = await mergeMusicUrls(projectRef, storedUrls);
|
||||
|
||||
console.log("🏷️ [SunoCallback] Projet marqué GENERATED", {
|
||||
projectId,
|
||||
musicUrlsCount: musicUrls.length,
|
||||
});
|
||||
|
||||
if (userId) {
|
||||
const successMessage =
|
||||
musicUrls.length > 0
|
||||
? `Ta musique pour "${projectTitle}" est prête.`
|
||||
: `La génération de musique pour "${projectTitle}" est terminée.`;
|
||||
try {
|
||||
console.log(`⬇️ [SunoCallback] Téléchargement piste ${index + 1}`);
|
||||
const resp = await axios.get(url, { responseType: "arraybuffer" });
|
||||
const buffer = Buffer.from(resp.data);
|
||||
const path = `users/${userId}/projects/${projectId}/task-${taskId}-${index + 1}.mp3`;
|
||||
const token = require("crypto").randomUUID();
|
||||
const file = bucket.file(path);
|
||||
await file.save(buffer, {
|
||||
resumable: false,
|
||||
metadata: {
|
||||
contentType: "audio/mpeg",
|
||||
cacheControl: "public, max-age=31536000",
|
||||
metadata: { firebaseStorageDownloadTokens: token },
|
||||
await sendNotification({
|
||||
sender: "SYSTEM",
|
||||
receiver: userId,
|
||||
receiverCollection: "users",
|
||||
title: "Musique prête",
|
||||
message: successMessage,
|
||||
data: {
|
||||
type: ALERT_TYPE?.MUSIC_GENERATION_SUCCESS,
|
||||
projectId,
|
||||
projectTitle,
|
||||
musicUrls,
|
||||
taskId,
|
||||
},
|
||||
});
|
||||
const downloadUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
|
||||
path,
|
||||
)}?alt=media&token=${token}`;
|
||||
console.log("✅ [SunoCallback] Sauvegardé:", path, "URL:", downloadUrl);
|
||||
return { path, url: downloadUrl };
|
||||
} catch (e) {
|
||||
} catch (notifError) {
|
||||
console.error(
|
||||
`❌ [SunoCallback] Échec save piste ${index + 1}:`,
|
||||
e.message,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const [p1, p2] = await Promise.all([
|
||||
saveOne(audioUrls[0], 0),
|
||||
saveOne(audioUrls[1], 1),
|
||||
]);
|
||||
|
||||
// 4) Mettre à jour le statut du projet
|
||||
try {
|
||||
const newMusicUrls = [p1?.url, p2?.url]
|
||||
.filter((url) => typeof url === "string" && url.trim())
|
||||
.map((url) => url.trim());
|
||||
|
||||
const projectRef = refList.projects.doc(projectId);
|
||||
let existingMusicUrls = [];
|
||||
try {
|
||||
const projectSnap = await projectRef.get();
|
||||
const projectData = projectSnap?.data() || {};
|
||||
if (Array.isArray(projectData.musicUrls)) {
|
||||
existingMusicUrls = projectData.musicUrls
|
||||
.filter((url) => typeof url === "string" && url.trim())
|
||||
.map((url) => url.trim());
|
||||
}
|
||||
} catch (readError) {
|
||||
console.error(
|
||||
"⚠️ [SunoCallback] Impossible de récupérer les anciennes musiques:",
|
||||
readError,
|
||||
"[sunoCallback] Failed to send success notification:",
|
||||
notifError,
|
||||
);
|
||||
}
|
||||
|
||||
const mergedMusicUrls = [...existingMusicUrls, ...newMusicUrls];
|
||||
const uniqueMusicUrls = mergedMusicUrls.filter(
|
||||
(url, index) => mergedMusicUrls.indexOf(url) === index,
|
||||
);
|
||||
const musicUrls =
|
||||
uniqueMusicUrls.length > MAX_STORED_MUSIC_TRACKS
|
||||
? uniqueMusicUrls.slice(
|
||||
uniqueMusicUrls.length - MAX_STORED_MUSIC_TRACKS,
|
||||
)
|
||||
: uniqueMusicUrls;
|
||||
|
||||
await projectRef.set(
|
||||
{
|
||||
musicStatus: "GENERATED",
|
||||
musicUrls,
|
||||
musicError: FieldValue.delete(),
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
console.log("🏷️ [SunoCallback] Projet marqué GENERATED", {
|
||||
projectId,
|
||||
musicUrlsCount: musicUrls.length,
|
||||
});
|
||||
|
||||
if (userId) {
|
||||
const successMessage =
|
||||
musicUrls.length > 0
|
||||
? `Ta musique pour "${projectTitle}" est prête.`
|
||||
: `La génération de musique pour "${projectTitle}" est terminée.`;
|
||||
try {
|
||||
await sendNotification({
|
||||
sender: "SYSTEM",
|
||||
receiver: userId,
|
||||
receiverCollection: "users",
|
||||
title: "Musique prête",
|
||||
message: successMessage,
|
||||
data: {
|
||||
type: ALERT_TYPE?.MUSIC_GENERATION_SUCCESS,
|
||||
projectId,
|
||||
projectTitle,
|
||||
musicUrls,
|
||||
taskId,
|
||||
},
|
||||
});
|
||||
} catch (notifError) {
|
||||
console.error(
|
||||
"[sunoCallback] Failed to send success notification:",
|
||||
notifError,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("❌ [SunoCallback] Erreur maj projet:", e.message);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
projectId,
|
||||
saved: [p1, p2].filter(Boolean),
|
||||
savedCount: storedUrls.length,
|
||||
musicUrlsCount: musicUrls.length,
|
||||
});
|
||||
} catch (error) {
|
||||
const statusCode =
|
||||
error?.message === "PROJECT_NOT_FOUND_FOR_TASK" ? 404 : 500;
|
||||
logger.error("❌ [SunoCallback] Erreur interne:", error);
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: "Erreur interne du serveur", message: error.message });
|
||||
return res.status(statusCode).json({
|
||||
success: false,
|
||||
error: error?.message || "Erreur interne du serveur",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user