fix list and improve cloud functions
This commit is contained in:
+10
-2
@@ -21,7 +21,11 @@ async function performCoverGeneration(project) {
|
||||
let coverUrl = "";
|
||||
try {
|
||||
// Utiliser une taille fixe de 1024 pixels
|
||||
coverUrl = await generateImageV2(prompt, 1024);
|
||||
coverUrl = await generateImageV2(
|
||||
prompt,
|
||||
1024,
|
||||
`users/${project.userId}/projects/${project.id}/generated-${Date.now()}.png`,
|
||||
);
|
||||
console.log("coverUrl", coverUrl);
|
||||
} catch (e) {
|
||||
logger.error("❌ [Cover] generateImageV2 failed", {
|
||||
@@ -73,7 +77,11 @@ async function performCombineGeneration(project) {
|
||||
fgPreview: String(foreground).slice(0, 80),
|
||||
});
|
||||
|
||||
const combinedUrl = await CombineCoverAndPicture(background, foreground);
|
||||
const combinedUrl = await CombineCoverAndPicture(
|
||||
background,
|
||||
foreground,
|
||||
`users/${project.userId}/projects/${project.id}/combine-${Date.now()}.png`,
|
||||
);
|
||||
|
||||
// Sauvegarder le résultat et marquer comme généré
|
||||
await db
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
const { onObjectFinalized } = require("firebase-functions/v2/storage");
|
||||
const functions = require("firebase-functions");
|
||||
const admin = require("firebase-admin");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
|
||||
// These deps must be installed in functions/: ffmpeg-static, fluent-ffmpeg
|
||||
let ffmpeg;
|
||||
let ffmpegPath;
|
||||
try {
|
||||
ffmpeg = require("fluent-ffmpeg");
|
||||
ffmpegPath = require("ffmpeg-static");
|
||||
ffmpeg.setFfmpegPath(ffmpegPath);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"ffmpeg modules not installed yet — deploy after npm i",
|
||||
e?.message
|
||||
);
|
||||
}
|
||||
|
||||
const bucket = admin.storage().bucket();
|
||||
|
||||
exports.transcodePlaybackToHLS = onObjectFinalized(
|
||||
{
|
||||
region: "europe-west1",
|
||||
memory: "2GiB",
|
||||
timeoutSeconds: 540,
|
||||
cpu: 2,
|
||||
},
|
||||
async (event) => {
|
||||
const object = event.data;
|
||||
const filePath = object?.name || ""; // e.g., musics/{projectId}/source.mp4
|
||||
const contentType = object?.contentType || "";
|
||||
|
||||
try {
|
||||
// Only process our target path + video files
|
||||
const match = filePath.match(/^musics\/(.+?)\/source\.mp4$/i);
|
||||
if (!match || !contentType.startsWith("video/")) {
|
||||
return;
|
||||
}
|
||||
const projectId = match[1];
|
||||
const tempDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), `hls_${projectId}_`)
|
||||
);
|
||||
const localSource = path.join(tempDir, "source.mp4");
|
||||
const hlsDir = path.join(tempDir, "hls");
|
||||
fs.mkdirSync(hlsDir);
|
||||
|
||||
// Download source
|
||||
await bucket.file(filePath).download({ destination: localSource });
|
||||
|
||||
if (!ffmpeg) {
|
||||
console.warn("ffmpeg not available; skip transcode");
|
||||
return;
|
||||
}
|
||||
|
||||
// Transcode to HLS (single rendition for simplicity)
|
||||
const masterPath = path.join(hlsDir, "master.m3u8");
|
||||
const segmentPattern = path.join(hlsDir, "segment_%03d.ts");
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
ffmpeg(localSource)
|
||||
.outputOptions([
|
||||
"-profile:v baseline",
|
||||
"-level 3.0",
|
||||
"-start_number 0",
|
||||
"-hls_time 4",
|
||||
"-hls_list_size 0",
|
||||
"-f hls",
|
||||
`-hls_segment_filename ${segmentPattern}`,
|
||||
])
|
||||
.output(masterPath)
|
||||
.on("end", resolve)
|
||||
.on("error", reject)
|
||||
.run();
|
||||
});
|
||||
|
||||
// Upload all files in hlsDir to Storage under musics/{projectId}/hls/
|
||||
const fileNames = fs.readdirSync(hlsDir);
|
||||
const destPrefix = `musics/${projectId}/hls/`;
|
||||
|
||||
// Upload all segments first
|
||||
const uploads = fileNames
|
||||
.filter((n) => n.endsWith(".ts"))
|
||||
.map((name) => {
|
||||
const local = path.join(hlsDir, name);
|
||||
const dest = `${destPrefix}${name}`;
|
||||
return bucket.upload(local, {
|
||||
destination: dest,
|
||||
metadata: {
|
||||
contentType: "video/MP2T",
|
||||
cacheControl: "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
});
|
||||
await Promise.all(uploads);
|
||||
|
||||
// Generate signed URLs for segments and rewrite playlist with absolute URLs
|
||||
const segmentNames = fileNames.filter((n) => n.endsWith(".ts"));
|
||||
const signedSegments = {};
|
||||
await Promise.all(
|
||||
segmentNames.map(async (name) => {
|
||||
const file = bucket.file(`${destPrefix}${name}`);
|
||||
const [url] = await file.getSignedUrl({
|
||||
action: "read",
|
||||
expires: Date.now() + 1000 * 60 * 60 * 24 * 365,
|
||||
});
|
||||
signedSegments[name] = url;
|
||||
})
|
||||
);
|
||||
|
||||
// Rewrite master playlist to use absolute signed segment URLs
|
||||
let masterContent = fs.readFileSync(masterPath, "utf8");
|
||||
masterContent = masterContent
|
||||
.split(/\r?\n/)
|
||||
.map((line) =>
|
||||
line.endsWith(".ts") && signedSegments[line]
|
||||
? signedSegments[line]
|
||||
: line
|
||||
)
|
||||
.join("\n");
|
||||
const rewrittenMasterLocal = path.join(hlsDir, "master.abs.m3u8");
|
||||
fs.writeFileSync(rewrittenMasterLocal, masterContent, "utf8");
|
||||
|
||||
// Upload rewritten playlist
|
||||
await bucket.upload(rewrittenMasterLocal, {
|
||||
destination: `${destPrefix}master.m3u8`,
|
||||
metadata: {
|
||||
contentType: "application/vnd.apple.mpegurl",
|
||||
cacheControl: "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
|
||||
// Compute signed URL for playlist
|
||||
const [signedUrl] = await bucket
|
||||
.file(`${destPrefix}master.m3u8`)
|
||||
.getSignedUrl({
|
||||
action: "read",
|
||||
expires: Date.now() + 1000 * 60 * 60 * 24 * 365,
|
||||
});
|
||||
|
||||
// Update Firestore project doc with playbackUrl
|
||||
await admin.firestore().collection("projects").doc(projectId).set(
|
||||
{
|
||||
playbackUrl: signedUrl,
|
||||
playbackStatus: "ready",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
);
|
||||
|
||||
// Cleanup tmp dir
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
console.error("HLS transcode error", e);
|
||||
}
|
||||
}
|
||||
);
|
||||
+10
-12
@@ -490,18 +490,16 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
|
||||
|
||||
// 1) Récupérer le projectId associé au taskId
|
||||
let projectId = null;
|
||||
try {
|
||||
const projSnap = await db
|
||||
.collection("projects")
|
||||
.where("sunoTaskId", "==", taskId)
|
||||
.limit(1)
|
||||
.get();
|
||||
if (!projSnap.empty) {
|
||||
projectId = projSnap.docs[0].id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("❌ [SunoCallback] Erreur lookup project par taskId:", e);
|
||||
const projSnap = await db
|
||||
.collection("projects")
|
||||
.where("sunoTaskId", "==", taskId)
|
||||
.limit(1)
|
||||
.get();
|
||||
if (projSnap.empty) {
|
||||
throw new Error("Aucun projet trouvé pour ce taskId");
|
||||
}
|
||||
projectId = projSnap.docs[0].id;
|
||||
const { userId } = projSnap.docs[0].data() || {};
|
||||
|
||||
if (!projectId) {
|
||||
console.warn("⚠️ [SunoCallback] Aucun projet trouvé pour", { taskId });
|
||||
@@ -540,7 +538,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
|
||||
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 = `musics/${projectId}/sound${index + 1}.mp3`;
|
||||
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, {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
const admin = require("firebase-admin");
|
||||
const { onDocumentWritten } = require("firebase-functions/firestore");
|
||||
const { refList } = require("../index");
|
||||
const { getSunoTimestamps } = require("./lyrics");
|
||||
const { deleteFolder } = require("../helpers/firebase");
|
||||
|
||||
exports.onProjectUpdate = onDocumentWritten(
|
||||
"projects/{projectId}",
|
||||
@@ -13,6 +16,24 @@ exports.onProjectUpdate = onDocumentWritten(
|
||||
if (!previousData?.songUrl && currentData?.songUrl) {
|
||||
await getSunoTimestamps(projectId);
|
||||
}
|
||||
|
||||
if (!currentData) {
|
||||
const { userId } = previousData || {};
|
||||
|
||||
//delete folder
|
||||
await deleteFolder(`users/${userId}/projects/${projectId}/`);
|
||||
|
||||
//delete tasks
|
||||
const snapshot = await refList.tasks
|
||||
.where("projectId", "==", projectId)
|
||||
.get();
|
||||
if (snapshot.empty) return null;
|
||||
const batch = admin.firestore().batch();
|
||||
snapshot.forEach((doc) => {
|
||||
batch.delete(doc.ref);
|
||||
});
|
||||
await batch.commit();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ exports.generateVideoThumbnail = onObjectFinalized(
|
||||
// Use unique folder under /tmp to avoid name collisions
|
||||
const tmpDir = path.join(
|
||||
os.tmpdir(),
|
||||
`thumb-${Date.now()}-${Math.round(Math.random() * 1000)}`
|
||||
`thumb-${Date.now()}-${Math.round(Math.random() * 1000)}`,
|
||||
);
|
||||
const baseName = path.basename(objectName);
|
||||
const dirName = path.posix.dirname(objectName);
|
||||
@@ -96,7 +96,7 @@ exports.generateVideoThumbnail = onObjectFinalized(
|
||||
thumbnailUrl,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
{ merge: true },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,5 +115,5 @@ exports.generateVideoThumbnail = onObjectFinalized(
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const admin = require("firebase-admin");
|
||||
const { onDocumentDeleted } = require("firebase-functions/firestore");
|
||||
const { refList } = require("../index");
|
||||
const { deleteFolder } = require("../helpers/firebase");
|
||||
|
||||
exports.onUserDelete = onDocumentDeleted("users/{userID}", async (event) => {
|
||||
try {
|
||||
const userID = event?.params?.userID;
|
||||
await clearAllUserData(userID);
|
||||
|
||||
await deleteFolder(`users/${userID}/`);
|
||||
|
||||
await admin.auth().deleteUser(userID);
|
||||
console.log(`User ${userID} deleted successfully`);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
|
||||
async function clearAllUserData(userID) {
|
||||
const deleteAll = async (ref, key, operator = "==") => {
|
||||
const snapshot = await ref.where(key, operator, userID).get();
|
||||
snapshot.forEach((item) => item.ref.delete());
|
||||
};
|
||||
const removeFromArray = async (ref, arrayName) => {
|
||||
const snapshot = await ref.where(arrayName, "array-contains", userID).get();
|
||||
snapshot.forEach((item) =>
|
||||
item.ref.update({
|
||||
[arrayName]: admin.firestore.FieldValue.arrayRemove(userID),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
await deleteAll(refList.projects, "userId");
|
||||
await deleteAll(refList.playlists, "createdBy");
|
||||
}
|
||||
Reference in New Issue
Block a user