Merge branch 'main' of gitlab.com:agenceminuit/musicland
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
exports.BATCH_TYPE = {
|
||||
ADD: "ADD",
|
||||
UPDATE: "UPDATE",
|
||||
DELETE: "DELETE",
|
||||
};
|
||||
+85
-37
@@ -13,49 +13,97 @@ function basicTemplate({ title = "", content = "", button = null }) {
|
||||
button?.label || "Ouvrir"
|
||||
}</a></p>`
|
||||
: "";
|
||||
return `<!doctype html><html><body style="font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.5;color:#111">
|
||||
<div style="max-width:640px;margin:40px auto;padding:24px;border:1px solid #eee;border-radius:12px">
|
||||
<div style="text-align:center;margin-bottom:20px">
|
||||
<img src="${musicLandLogoSrc}" alt="MusicLand" style="height:40px" />
|
||||
</div>
|
||||
<h2 style="margin:0 0 16px">${title}</h2>
|
||||
<div>${content}</div>
|
||||
${btn}
|
||||
<hr style="margin:24px 0;border:none;border-top:1px solid #eee" />
|
||||
<p style="color:#666;font-size:12px">minuit.app</p>
|
||||
</div>
|
||||
return `<!doctype html><html lang="fr" style="background-color:#0b0b10"><head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="supported-color-schemes" content="dark" />
|
||||
<style>
|
||||
:root { color-scheme: dark; supported-color-schemes: dark; }
|
||||
body { margin:0 !important; background-color:#0b0b10 !important; }
|
||||
body, .email-surface { background-color:#0b0b10 !important; color:#ffffff !important; }
|
||||
u + .body .email-surface { background-color:#0b0b10 !important; color:#ffffff !important; }
|
||||
u + .body .email-content,
|
||||
[data-ogsc] .email-content,
|
||||
[data-ogsb] .email-content { background-color:#151520 !important; color:#ffffff !important; }
|
||||
u + .body .email-content *,
|
||||
[data-ogsc] .email-content *,
|
||||
[data-ogsb] .email-content * { color:#ffffff !important; }
|
||||
</style>
|
||||
</head><body class="body" data-ogsc="dark" data-ogsb="dark" bgcolor="#0b0b10" style="margin:0;padding:0;background-color:#0b0b10!important;color:#ffffff!important;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.5">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" class="email-surface" style="border-collapse:collapse;background-color:#0b0b10">
|
||||
<tr>
|
||||
<td align="center" style="padding:40px 16px">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" bgcolor="#151520" class="email-content" style="max-width:640px;border-radius:12px;background-color:#151520;border:1px solid rgba(255,255,255,0.08);box-shadow:0 12px 40px rgba(0,0,0,0.35);overflow:hidden">
|
||||
<tr>
|
||||
<td bgcolor="#151520" style="padding:24px;background-color:#151520;color:#ffffff">
|
||||
<div style="text-align:center;margin-bottom:20px;color:#ffffff">
|
||||
<img src="${musicLandLogoSrc}" alt="MusicLand" style="height:40px" />
|
||||
</div>
|
||||
<h2 style="margin:0 0 16px;font-size:24px;color:#ffffff!important">${title}</h2>
|
||||
<div style="color:rgba(255,255,255,0.9)!important">${content}</div>
|
||||
${btn}
|
||||
<hr style="margin:24px 0;border:none;border-top:1px solid rgba(255,255,255,0.1)" />
|
||||
<p style="color:rgba(255,255,255,0.45);font-size:12px;margin:0">minuit.app</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
function welcomeTemplate({ firstName = "", lastName = "" }) {
|
||||
const fullName = [firstName, lastName].filter(Boolean).join(" ").trim();
|
||||
const greeting = fullName ? `Salut ${fullName},` : "Salut,";
|
||||
return `<!doctype html><html><body style="margin:0;padding:0;background:#0b0b10;color:#fff;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif">
|
||||
<div style="max-width:640px;margin:0 auto;padding:40px 24px">
|
||||
<div style="background:#151520;border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,0.08)">
|
||||
<div style="padding:32px 28px">
|
||||
<div style="text-align:center;margin-bottom:24px">
|
||||
<img
|
||||
src="${musicLandLogoSrc}"
|
||||
alt="MusicLand"
|
||||
style="display:block;margin:0 auto;width:180px;max-width:100%;height:auto"
|
||||
/>
|
||||
</div>
|
||||
<p style="margin:0 0 12px;font-size:16px;letter-spacing:0.2px">${greeting}</p>
|
||||
<h1 style="margin:0 0 16px;font-size:28px;line-height:1.2">Bienvenue sur MusicLand</h1>
|
||||
<p style="margin:0 0 16px;font-size:16px;line-height:1.6;color:rgba(255,255,255,0.85)">
|
||||
Nous sommes ravis de t'accueillir dans la communauté des artistes et passionnés de MusicLand.
|
||||
</p>
|
||||
<p style="margin:0 0 16px;font-size:15px;line-height:1.6;color:rgba(255,255,255,0.75)">
|
||||
Explore les playlists collaboratives, dépose tes projets et découvre ce que prépare la scène émergente.
|
||||
</p>
|
||||
<p style="margin:0;font-size:15px;line-height:1.6;color:rgba(255,255,255,0.75)">
|
||||
L'équipe MusicLand
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin:32px 0 0;text-align:center;font-size:12px;color:rgba(255,255,255,0.45)">minuit.app · Paris, France</p>
|
||||
</div>
|
||||
return `<!doctype html><html lang="fr" style="background-color:#0b0b10"><head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="supported-color-schemes" content="dark" />
|
||||
<style>
|
||||
:root { color-scheme: dark; supported-color-schemes: dark; }
|
||||
body { margin:0 !important; background-color:#0b0b10 !important; }
|
||||
body, .email-surface { background-color:#0b0b10 !important; color:#ffffff !important; }
|
||||
u + .body .email-surface { background-color:#0b0b10 !important; color:#ffffff !important; }
|
||||
u + .body .email-content,
|
||||
[data-ogsc] .email-content,
|
||||
[data-ogsb] .email-content { background-color:#151520 !important; color:#ffffff !important; }
|
||||
u + .body .email-content *,
|
||||
[data-ogsc] .email-content *,
|
||||
[data-ogsb] .email-content * { color:#ffffff !important; }
|
||||
</style>
|
||||
</head><body class="body" data-ogsc="dark" data-ogsb="dark" bgcolor="#0b0b10" style="margin:0;padding:0;background-color:#0b0b10!important;color:#ffffff!important;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.5">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" class="email-surface" style="border-collapse:collapse;background-color:#0b0b10">
|
||||
<tr>
|
||||
<td align="center" style="padding:40px 16px">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" bgcolor="#151520" class="email-content" style="max-width:640px;border-radius:16px;overflow:hidden;background-color:#151520;border:1px solid rgba(255,255,255,0.08);box-shadow:0 12px 40px rgba(0,0,0,0.35)">
|
||||
<tr>
|
||||
<td bgcolor="#151520" style="padding:32px 28px;background-color:#151520;color:#ffffff">
|
||||
<div style="text-align:center;margin-bottom:24px;color:#ffffff">
|
||||
<img
|
||||
src="${musicLandLogoSrc}"
|
||||
alt="MusicLand"
|
||||
style="display:block;margin:0 auto;width:180px;max-width:100%;height:auto"
|
||||
/>
|
||||
</div>
|
||||
<p style="margin:0 0 12px;font-size:16px;letter-spacing:0.2px;color:#ffffff!important">${greeting}</p>
|
||||
<h1 style="margin:0 0 16px;font-size:28px;line-height:1.2;color:#ffffff!important">Bienvenue sur MusicLand</h1>
|
||||
<p style="margin:0 0 16px;font-size:16px;line-height:1.6;color:#ffffff!important">
|
||||
Nous sommes ravis de t'accueillir dans la communauté des artistes et passionnés de MusicLand.
|
||||
</p>
|
||||
<p style="margin:0 0 16px;font-size:15px;line-height:1.6;color:#ffffff!important">
|
||||
Explore les playlists collaboratives, dépose tes projets et découvre ce que prépare la scène émergente.
|
||||
</p>
|
||||
<p style="margin:0;font-size:15px;line-height:1.6;color:#ffffff!important">
|
||||
L'équipe MusicLand
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin:32px 0 0;text-align:center;font-size:12px;color:rgba(255,255,255,0.6)">minuit.app · Paris, France</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const admin = require("firebase-admin");
|
||||
const { BATCH_TYPE } = require("../config/types");
|
||||
|
||||
async function deleteFolder(path) {
|
||||
try {
|
||||
@@ -11,4 +12,77 @@ async function deleteFolder(path) {
|
||||
}
|
||||
}
|
||||
|
||||
async function batchFirestore({
|
||||
path = "", // ADD only
|
||||
docs = [], // not for ADD
|
||||
data = {}, // not for DELETE
|
||||
type = BATCH_TYPE.UPDATE,
|
||||
}) {
|
||||
try {
|
||||
if (docs?.length === 0 && type !== BATCH_TYPE.ADD) {
|
||||
console.log(`Aucun document trouvé dans la collection ${path}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Préparer des lots pour les opérations
|
||||
const batches = [];
|
||||
let currentBatch = admin.firestore().batch();
|
||||
let operationCounter = 0;
|
||||
|
||||
docs.forEach((doc) => {
|
||||
const ref =
|
||||
doc?.ref ||
|
||||
(typeof doc?.path === "string"
|
||||
? admin.firestore().doc(doc.path)
|
||||
: null);
|
||||
const payload =
|
||||
doc && typeof doc.data === "object" && doc.data !== null && !Array.isArray(doc.data)
|
||||
? doc.data
|
||||
: data;
|
||||
|
||||
if (type === BATCH_TYPE.ADD) {
|
||||
// Pour ADD, créer un nouveau document avec ID automatique
|
||||
const newDocRef = admin.firestore().collection(path).doc();
|
||||
currentBatch.set(newDocRef, data);
|
||||
} else if (type === BATCH_TYPE.UPDATE) {
|
||||
if (!ref) {
|
||||
throw new Error("batchFirestore UPDATE nécessite une référence de document.");
|
||||
}
|
||||
if (!payload || Object.keys(payload).length === 0) {
|
||||
throw new Error("batchFirestore UPDATE nécessite des données à écrire.");
|
||||
}
|
||||
currentBatch.set(ref, payload, { merge: true });
|
||||
} else if (type === BATCH_TYPE.DELETE) {
|
||||
if (!ref) {
|
||||
throw new Error("batchFirestore DELETE nécessite une référence de document.");
|
||||
}
|
||||
currentBatch.delete(ref);
|
||||
} else {
|
||||
throw new Error(`Opération non supportée : ${type}`);
|
||||
}
|
||||
operationCounter++;
|
||||
// Si le lot atteint la limite de 500 opérations, le sauvegarder et en créer un nouveau
|
||||
if (operationCounter === 500) {
|
||||
batches.push(currentBatch);
|
||||
currentBatch = admin.firestore().batch();
|
||||
operationCounter = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter le dernier lot si des opérations y sont présentes
|
||||
if (operationCounter > 0) {
|
||||
batches.push(currentBatch);
|
||||
}
|
||||
|
||||
// Exécuter tous les lots en parallèle
|
||||
await Promise.all(batches.map((batch) => batch.commit()));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Erreur lors des opérations ${type} pour la collection ${path} :`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
exports.batchFirestore = batchFirestore;
|
||||
exports.deleteFolder = deleteFolder;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const normalizeDate = (input) =>
|
||||
typeof input?.toDate === "function" ? input.toDate() : new Date(input);
|
||||
|
||||
const buildMonthKey = (timestamp) => {
|
||||
const date = normalizeDate(timestamp);
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
return `${year}-${month}`;
|
||||
};
|
||||
|
||||
const buildPreviousMonthContext = (referenceDate) => {
|
||||
const current = referenceDate ? new Date(referenceDate) : new Date();
|
||||
current.setUTCHours(0, 0, 0, 0);
|
||||
current.setUTCDate(1);
|
||||
|
||||
const target = new Date(current);
|
||||
target.setUTCMonth(target.getUTCMonth() - 1);
|
||||
|
||||
const year = target.getUTCFullYear();
|
||||
const monthIndex = target.getUTCMonth();
|
||||
const rangeStart = new Date(Date.UTC(year, monthIndex, 1, 0, 0, 0, 0));
|
||||
const rangeEnd = new Date(Date.UTC(year, monthIndex + 1, 0, 23, 59, 59, 999));
|
||||
const monthKey = buildMonthKey(rangeStart);
|
||||
|
||||
return {
|
||||
year,
|
||||
month: monthIndex + 1,
|
||||
monthKey,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
buildMonthKey,
|
||||
buildPreviousMonthContext,
|
||||
};
|
||||
+20
-5
@@ -4,18 +4,33 @@ const admin = require("firebase-admin");
|
||||
// Avoid bundling a service account key and hardcoding project/bucket.
|
||||
admin.initializeApp();
|
||||
|
||||
const db = admin.firestore();
|
||||
|
||||
exports.db = db;
|
||||
|
||||
exports.refList = {
|
||||
projects: admin.firestore().collection("projects"),
|
||||
playlists: admin.firestore().collection("playlists"),
|
||||
tasks: admin.firestore().collection("tasks"),
|
||||
notifications: admin.firestore().collection("notifications"),
|
||||
users: admin.firestore().collection("users"),
|
||||
projects: db.collection("projects"),
|
||||
playlists: db.collection("playlists"),
|
||||
tasks: db.collection("tasks"),
|
||||
notifications: db.collection("notifications"),
|
||||
users: db.collection("users"),
|
||||
projectStreamStats: db.collection("projectStreamStats"),
|
||||
projectStreamStatsMonthlyTotals: db.collection(
|
||||
"projectStreamStatsMonthlyTotals",
|
||||
),
|
||||
monthlyPayoutEntries: db.collection("monthlyPayoutEntries"),
|
||||
monthlyPayouts: db.collection("monthlyPayouts"),
|
||||
};
|
||||
|
||||
exports.ALERT_TYPE = {
|
||||
NEW_LIKE: "NEW_LIKE",
|
||||
NEW_COMMENT: "NEW_COMMENT",
|
||||
NEW_FOLLOWER: "NEW_FOLLOWER",
|
||||
MUSIC_GENERATION_SUCCESS: "MUSIC_GENERATION_SUCCESS",
|
||||
MUSIC_GENERATION_FAILED: "MUSIC_GENERATION_FAILED",
|
||||
COVER_GENERATION_SUCCESS: "COVER_GENERATION_SUCCESS",
|
||||
COVER_GENERATION_FAILED: "COVER_GENERATION_FAILED",
|
||||
PAYOUT_AVAILABLE: "PAYOUT_AVAILABLE",
|
||||
};
|
||||
|
||||
// Exporter toutes les fonctions
|
||||
|
||||
Generated
+51
-42
@@ -12,10 +12,11 @@
|
||||
"axios": "^1.11.0",
|
||||
"expo-server-sdk": "^4.0.0",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"firebase-admin": "^12.7.0",
|
||||
"firebase-functions": "^6.4.0",
|
||||
"firebase-admin": "^12.6.0",
|
||||
"firebase-functions": "^6.0.1",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"genkit": "^1.18.0",
|
||||
"lodash": "^4.17.21",
|
||||
"resend": "^6.3.0",
|
||||
"sharp": "^0.33.4"
|
||||
},
|
||||
@@ -65,9 +66,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz",
|
||||
"integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz",
|
||||
"integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -355,12 +356,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@genkit-ai/ai": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@genkit-ai/ai/-/ai-1.21.0.tgz",
|
||||
"integrity": "sha512-DLAes+w3Yv9a4zgJUL5CS8eVrNvud/LRv+F4E/O3KkpWz0uoKYREIrTZYSrDt/pF/jlqkRDQaalf7sGKS44IuQ==",
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@genkit-ai/ai/-/ai-1.22.0.tgz",
|
||||
"integrity": "sha512-TDKO+zWyM5YI8zE4a0IlqlpgHuLB4B4islzgWDvzdQlbjtyJp0ayODAMFhS2ruQ6+a/UdXDySRrOX/RcqF4yjA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@genkit-ai/core": "1.21.0",
|
||||
"@genkit-ai/core": "1.22.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.11.19",
|
||||
"colorette": "^2.0.20",
|
||||
@@ -373,9 +374,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@genkit-ai/ai/node_modules/@types/node": {
|
||||
"version": "20.19.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
|
||||
"integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
|
||||
"version": "20.19.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
|
||||
"integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@@ -395,9 +396,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@genkit-ai/core": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@genkit-ai/core/-/core-1.21.0.tgz",
|
||||
"integrity": "sha512-suQ81HwtDObejkXJUBWzD5145qoHFXxdozxwzYDy8m93f7kKKtQwSJPQmYXWsTAdHJSjERHfKLCe/xrDshkPYw==",
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@genkit-ai/core/-/core-1.22.0.tgz",
|
||||
"integrity": "sha512-etVlpwJkPoy91xR6H5+S/AWZPJMeovb7N35+B90md1+6xWcodQF7WZ3chKcH31Xamlz+jTIvd3riiZGY9RFumg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
@@ -447,19 +448,19 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@genkit-ai/firebase": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@genkit-ai/firebase/-/firebase-1.21.0.tgz",
|
||||
"integrity": "sha512-i21IStUGFn10+25+o7HL2XPtXvb2FIxqtfVHLrpSFqsT8sMdRSNceVg5mU5MHY2yjhbgpfntbs1YDO2sf4QRew==",
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@genkit-ai/firebase/-/firebase-1.22.0.tgz",
|
||||
"integrity": "sha512-iTOVma8be8cOFzO9yaNuzBIICgFAP+N1cuB5T8K5LE5ahsSIG7sZWUE5FwMKtEDFk9D343kgLuvW9nxLkV7IzA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@genkit-ai/google-cloud": "^1.21.0"
|
||||
"@genkit-ai/google-cloud": "^1.22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@google-cloud/firestore": "^7.11.0",
|
||||
"firebase": ">=11.5.0",
|
||||
"firebase-admin": ">=12.2",
|
||||
"genkit": "^1.21.0"
|
||||
"genkit": "^1.22.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"firebase": {
|
||||
@@ -468,9 +469,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@genkit-ai/google-cloud": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@genkit-ai/google-cloud/-/google-cloud-1.21.0.tgz",
|
||||
"integrity": "sha512-DsNecaDnvH7NNv4NGxIFshjMzWzXXB4fD/CRM6aLQv0TGOTip/3J3+5iKNqeSpmqDv8jIJtrOlclwhKb6cR1Wg==",
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@genkit-ai/google-cloud/-/google-cloud-1.22.0.tgz",
|
||||
"integrity": "sha512-PVt7oLu3VARQ4u/R4VvG/q2lsEdTke9jxQI8sg1jxHoKV03fNUwKAyNd59ZT4igrQIUCqdJD4tH3Kzh4yd4QTw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -493,13 +494,13 @@
|
||||
"winston": "^3.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"genkit": "^1.21.0"
|
||||
"genkit": "^1.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@genkit-ai/googleai": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@genkit-ai/googleai/-/googleai-1.21.0.tgz",
|
||||
"integrity": "sha512-sJauWpr0vu0cnA05UwQAfd2Vh04+Lar1YC3Nn/YBbwBow3Y7KWAqQUNaGHSka/ZYiUHxlzjYyhaDtodmnObepw==",
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@genkit-ai/googleai/-/googleai-1.22.0.tgz",
|
||||
"integrity": "sha512-6GTfrsv2acRWlXcvi8jqH044tG/eslMQCH1wxpA//wzTnsnht5f4SJ3isKU3gcmyQtoYWiaUN8/F7OYqkIYHcw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.24.0",
|
||||
@@ -507,7 +508,7 @@
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"genkit": "^1.21.0"
|
||||
"genkit": "^1.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/common": {
|
||||
@@ -2630,6 +2631,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz",
|
||||
"integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "1.25.1",
|
||||
"@opentelemetry/resources": "1.25.1",
|
||||
@@ -2974,9 +2976,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.18.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz",
|
||||
"integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==",
|
||||
"version": "22.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz",
|
||||
"integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@@ -3320,9 +3322,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz",
|
||||
"integrity": "sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==",
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
|
||||
"integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@@ -4749,14 +4751,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/genkit": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/genkit/-/genkit-1.21.0.tgz",
|
||||
"integrity": "sha512-catTEjxhHZaicvxwak8jFL0K0H0ndN/qE9s+N7CIbsQeJczvRDleoyFa2mtaBOCaoZt7Xj8zJlYXOmt8tRyqJQ==",
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/genkit/-/genkit-1.22.0.tgz",
|
||||
"integrity": "sha512-GoVVO3EnNHrjkMkUPRvgx1MjBHKvOlZAu/ffMIJgLFxrH7rrUbvfHXE6Nk7uh5BNvET7+DApyhbhqz9G8sy+mQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@genkit-ai/ai": "1.21.0",
|
||||
"@genkit-ai/core": "1.21.0",
|
||||
"@genkit-ai/ai": "1.22.0",
|
||||
"@genkit-ai/core": "1.22.0",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
},
|
||||
@@ -5610,6 +5612,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
@@ -6436,9 +6444,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resend": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/resend/-/resend-6.3.0.tgz",
|
||||
"integrity": "sha512-Nmg5cFojC2E07gQ/RnlcroXpclFnlr05yQP2QNbbAhfX1Q4LBqiSd6/0t/fq95MvcuP8EDbb+Eyw1NG2P0OWgw==",
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/resend/-/resend-6.4.0.tgz",
|
||||
"integrity": "sha512-CTr4ix4RI5M/ucL58Wqr+LE8eI4JHtJEFaBAx6yUVNOI3eaPVtJjpNL0G/BdRSWMbwv6CtpprVOY8Xvpp6UJlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"svix": "1.76.1"
|
||||
@@ -7332,6 +7340,7 @@
|
||||
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.8",
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
"axios": "^1.11.0",
|
||||
"expo-server-sdk": "^4.0.0",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"firebase-admin": "^12.7.0",
|
||||
"firebase-functions": "^6.4.0",
|
||||
"firebase-admin": "^12.6.0",
|
||||
"firebase-functions": "^6.0.1",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"genkit": "^1.18.0",
|
||||
"lodash": "^4.17.21",
|
||||
"resend": "^6.3.0",
|
||||
"sharp": "^0.33.4"
|
||||
},
|
||||
|
||||
+86
-28
@@ -1,21 +1,17 @@
|
||||
const { onDocumentCreated } = require("firebase-functions/v2/firestore");
|
||||
const admin = require("firebase-admin");
|
||||
const logger = require("firebase-functions/logger");
|
||||
const {
|
||||
generateImageV2,
|
||||
} = require("../helpers/gemini");
|
||||
const { generateImageV2 } = require("../helpers/gemini");
|
||||
const { generatePicturePrompt } = require("../helpers/prompts");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const axios = require("axios");
|
||||
const sharp = require("sharp");
|
||||
const { ALERT_TYPE, refList } = require("../index");
|
||||
const { sendNotification } = require("./notifications");
|
||||
|
||||
const db = admin.firestore();
|
||||
const bucket = admin.storage().bucket();
|
||||
const LOGO_PATH = path.resolve(
|
||||
__dirname,
|
||||
"../assets/musicLandProduction.png",
|
||||
);
|
||||
const LOGO_PATH = path.resolve(__dirname, "../assets/musicLandProduction.png");
|
||||
|
||||
async function buildCoverWithLogo(backgroundUrl, targetPath) {
|
||||
logger.info("🖼️ [Cover] Adding logo to generated background");
|
||||
@@ -124,22 +120,19 @@ async function performCoverGeneration(project) {
|
||||
|
||||
const [firstOption] = options;
|
||||
|
||||
await db
|
||||
.collection("projects")
|
||||
.doc(project.id)
|
||||
.set(
|
||||
{
|
||||
cover: {
|
||||
generatedBackground: firstOption?.generatedUrl || null,
|
||||
result: firstOption?.finalUrl || null,
|
||||
selectedOptionId: firstOption?.id || null,
|
||||
options,
|
||||
},
|
||||
coverStatus: "GENERATED",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
await refList.projects.doc(project.id).set(
|
||||
{
|
||||
cover: {
|
||||
generatedBackground: firstOption?.generatedUrl || null,
|
||||
result: firstOption?.finalUrl || null,
|
||||
selectedOptionId: firstOption?.id || null,
|
||||
options,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
coverStatus: "GENERATED",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
logger.info("✅ [Cover] Saved", {
|
||||
projectId: project.id,
|
||||
@@ -162,6 +155,7 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
const type = data?.type || "";
|
||||
const projectId = data?.projectId || null;
|
||||
let coverUrl = null;
|
||||
let project = null;
|
||||
try {
|
||||
if (!projectId) {
|
||||
throw new Error("projectId manquant dans la task");
|
||||
@@ -189,13 +183,12 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("projects").doc(projectId).update({
|
||||
await refList.projects.doc(projectId).update({
|
||||
coverStatus: "GENERATING",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
||||
const project =
|
||||
(await db.collection("projects").doc(projectId).get())?.data() || null;
|
||||
project = (await refList.projects.doc(projectId).get())?.data() || null;
|
||||
if (!project) {
|
||||
throw new Error("Projet non trouvé");
|
||||
}
|
||||
@@ -217,7 +210,7 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
projectId,
|
||||
existingOptionsCount,
|
||||
});
|
||||
await db.collection("projects").doc(projectId).update({
|
||||
await refList.projects.doc(projectId).update({
|
||||
coverStatus: "GENERATED",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
@@ -244,6 +237,39 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
taskId: event?.params?.taskId,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (
|
||||
project?.userId &&
|
||||
typeof project.userId === "string" &&
|
||||
project.userId.trim()
|
||||
) {
|
||||
const receiverId = project.userId.trim();
|
||||
const projectTitle =
|
||||
typeof project?.title === "string" && project.title.trim()
|
||||
? project.title.trim()
|
||||
: "ton projet";
|
||||
const message = `Ta nouvelle pochette pour "${projectTitle}" est prête.`;
|
||||
try {
|
||||
await sendNotification({
|
||||
sender: "SYSTEM",
|
||||
receiver: receiverId,
|
||||
receiverCollection: "users",
|
||||
title: "Pochette générée",
|
||||
message,
|
||||
data: {
|
||||
type: ALERT_TYPE?.COVER_GENERATION_SUCCESS,
|
||||
projectId,
|
||||
projectTitle,
|
||||
coverUrl,
|
||||
},
|
||||
});
|
||||
} catch (notifError) {
|
||||
logger.error("❌ [Cover] Failed to send success notification", {
|
||||
projectId,
|
||||
error: notifError?.message || String(notifError),
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
logger.error("❌ [Task] Cover generation error", {
|
||||
@@ -256,9 +282,41 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
{ status: "ERROR", error: error?.message || "Erreur" },
|
||||
{ merge: true },
|
||||
);
|
||||
await db.collection("projects").doc(projectId).update({
|
||||
await refList.projects.doc(projectId).update({
|
||||
coverStatus: "ERROR",
|
||||
});
|
||||
if (
|
||||
project?.userId &&
|
||||
typeof project.userId === "string" &&
|
||||
project.userId.trim()
|
||||
) {
|
||||
const receiverId = project.userId.trim();
|
||||
const projectTitle =
|
||||
typeof project?.title === "string" && project.title.trim()
|
||||
? project.title.trim()
|
||||
: "ton projet";
|
||||
const message = `La génération de la pochette pour "${projectTitle}" a échoué.`;
|
||||
try {
|
||||
await sendNotification({
|
||||
sender: "SYSTEM",
|
||||
receiver: receiverId,
|
||||
receiverCollection: "users",
|
||||
title: "Pochette indisponible",
|
||||
message,
|
||||
data: {
|
||||
type: ALERT_TYPE?.COVER_GENERATION_FAILED,
|
||||
projectId,
|
||||
projectTitle,
|
||||
error: error?.message || String(error),
|
||||
},
|
||||
});
|
||||
} catch (notifError) {
|
||||
logger.error("❌ [Cover] Failed to send error notification", {
|
||||
projectId,
|
||||
error: notifError?.message || String(notifError),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
+95
-22
@@ -1,7 +1,13 @@
|
||||
const { onCall, onRequest, HttpsError } = 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");
|
||||
const { ALERT_TYPE, refList } = require("../index");
|
||||
const { sendNotification } = require("./notifications");
|
||||
const { SUNO_API_KEY } = require("../config/keys");
|
||||
const {
|
||||
SUNO_MODEL,
|
||||
@@ -11,9 +17,6 @@ const {
|
||||
SUNO_STATUS_PATH,
|
||||
} = require("../config/suno");
|
||||
|
||||
// Initialiser Firestorey
|
||||
const db = admin.firestore();
|
||||
|
||||
/**
|
||||
* Marque un projet comme échoué suite à une erreur Suno
|
||||
* @param {string} projectId - Identifiant du projet
|
||||
@@ -22,7 +25,9 @@ const db = admin.firestore();
|
||||
async function markProjectMusicFailure(projectId, error) {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
const docRef = db.collection("projects").doc(projectId);
|
||||
const docRef = refList.projects.doc(projectId);
|
||||
const projectSnap = await docRef.get();
|
||||
const projectData = projectSnap?.data() || {};
|
||||
const status = error?.response?.status || error?.status || null;
|
||||
const sunoMessage =
|
||||
error?.response?.data?.msg ||
|
||||
@@ -44,12 +49,44 @@ async function markProjectMusicFailure(projectId, error) {
|
||||
musicError: errorPayload,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
const receiverId =
|
||||
typeof projectData?.userId === "string" && projectData.userId.trim()
|
||||
? projectData.userId.trim()
|
||||
: null;
|
||||
if (receiverId) {
|
||||
const projectTitle =
|
||||
typeof projectData?.title === "string" && projectData.title.trim()
|
||||
? projectData.title.trim()
|
||||
: "ton projet";
|
||||
const message = `La génération de musique pour "${projectTitle}" a échoué.`;
|
||||
try {
|
||||
await sendNotification({
|
||||
sender: "SYSTEM",
|
||||
receiver: receiverId,
|
||||
receiverCollection: "users",
|
||||
title: "Génération de musique échouée",
|
||||
message,
|
||||
data: {
|
||||
type: ALERT_TYPE?.MUSIC_GENERATION_FAILED,
|
||||
projectId,
|
||||
projectTitle,
|
||||
error: errorPayload,
|
||||
},
|
||||
});
|
||||
} catch (notifyError) {
|
||||
console.error(
|
||||
"[markProjectMusicFailure] Failed to send notification:",
|
||||
notifyError,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"❌ [generateMusic] Impossible de marquer le projet en erreur:",
|
||||
err
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +117,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 [];
|
||||
@@ -134,7 +171,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);
|
||||
@@ -346,7 +383,7 @@ exports.generateMusic = onCall(async ({ data = {} }) => {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${SUNO_API_KEY}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const parsed = response.data;
|
||||
@@ -370,7 +407,7 @@ exports.generateMusic = onCall(async ({ data = {} }) => {
|
||||
} catch (markErr) {
|
||||
console.error(
|
||||
"❌ Erreur lors de la mise à jour du statut de projet:",
|
||||
markErr
|
||||
markErr,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -412,7 +449,7 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => {
|
||||
Authorization: `Bearer ${SUNO_API_KEY}`,
|
||||
},
|
||||
timeout: 30000, // 30 secondes de timeout
|
||||
}
|
||||
},
|
||||
);
|
||||
parsed = response.data;
|
||||
|
||||
@@ -547,16 +584,24 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
|
||||
|
||||
// 1) Récupérer le projectId associé au taskId
|
||||
let projectId = null;
|
||||
const projSnap = await db
|
||||
.collection("projects")
|
||||
const projSnap = await refList.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() || {};
|
||||
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 });
|
||||
@@ -567,7 +612,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);
|
||||
@@ -582,7 +627,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
|
||||
has_audio_url: !!t.audio_url,
|
||||
has_stream_audio_url: !!t.stream_audio_url,
|
||||
})),
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -607,14 +652,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;
|
||||
}
|
||||
@@ -628,19 +673,47 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
|
||||
// 4) Mettre à jour le statut du projet
|
||||
try {
|
||||
const musicUrls = [p1?.url, p2?.url].filter(Boolean);
|
||||
await db.collection("projects").doc(projectId).set(
|
||||
await refList.projects.doc(projectId).set(
|
||||
{
|
||||
musicStatus: "GENERATED",
|
||||
musicUrls,
|
||||
musicError: admin.firestore.FieldValue.delete(),
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
{ 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);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,16 @@ const resendInstance = new Resend(RESEND_API_KEY);
|
||||
|
||||
// Initialisation de Expo SDK
|
||||
let expo = new Expo();
|
||||
const db = admin.firestore();
|
||||
|
||||
const EMAIL_FROM = "MusicLand <musicland@minuit.app>";
|
||||
|
||||
function getCollectionRef(collectionName = "") {
|
||||
const ref = refList?.[collectionName];
|
||||
if (!ref) {
|
||||
throw new Error(`Unknown collection "${collectionName}"`);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
|
||||
{ region: "europe-west1", document: "notifications/{notificationId}" },
|
||||
async (event) => {
|
||||
@@ -34,12 +40,17 @@ exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
|
||||
throw new Error("Receiver and message are required");
|
||||
}
|
||||
|
||||
const receiverSnap = await getCollectionRef(receiverCollection)
|
||||
.doc(receiver)
|
||||
.get();
|
||||
const receiverData = receiverSnap.exists ? receiverSnap.data() : {};
|
||||
|
||||
const {
|
||||
pushToken = null,
|
||||
pushTokens = [],
|
||||
email = "",
|
||||
emailNotifications = false,
|
||||
} = (await db.collection(receiverCollection).doc(receiver).get()).data();
|
||||
} = receiverData;
|
||||
|
||||
if (!mailOnly) {
|
||||
const tokensSet = new Set(
|
||||
@@ -193,7 +204,7 @@ async function removeInvalidTokens({
|
||||
return;
|
||||
}
|
||||
|
||||
const docRef = db.collection(receiverCollection).doc(receiverId);
|
||||
const docRef = getCollectionRef(receiverCollection).doc(receiverId);
|
||||
const userSnap = await docRef.get();
|
||||
const userData = userSnap?.data() || {};
|
||||
|
||||
|
||||
+142
-64
@@ -1,36 +1,17 @@
|
||||
const admin = require("firebase-admin");
|
||||
const { onSchedule } = require("firebase-functions/v2/scheduler");
|
||||
const { refList } = require("../index");
|
||||
|
||||
const firestore = refList.projects.firestore;
|
||||
const _ = require("lodash");
|
||||
const { refList, db, ALERT_TYPE } = require("../index");
|
||||
const { batchFirestore } = require("../helpers/firebase");
|
||||
const { BATCH_TYPE } = require("../config/types");
|
||||
const {
|
||||
buildMonthKey,
|
||||
buildPreviousMonthContext,
|
||||
} = require("../helpers/stats");
|
||||
const { sendNotification } = require("./notifications");
|
||||
|
||||
const DISTRIBUTION_REVENUE_BASELINE = 1000;
|
||||
const DISTRIBUTION_RATIO = 0.3;
|
||||
const MAX_RECIPIENTS = 10;
|
||||
const WEIGHTS = Array.from({ length: MAX_RECIPIENTS }, (_, idx) => MAX_RECIPIENTS - idx);
|
||||
|
||||
const buildPreviousMonthContext = (referenceDate) => {
|
||||
const current = referenceDate ? new Date(referenceDate) : new Date();
|
||||
current.setHours(1, 0, 0, 0);
|
||||
current.setDate(1);
|
||||
|
||||
const target = new Date(current);
|
||||
target.setMonth(target.getMonth() - 1);
|
||||
|
||||
const month = target.getMonth();
|
||||
const year = target.getFullYear();
|
||||
const monthKey = `${year}-${String(month + 1).padStart(2, "0")}`;
|
||||
const rangeStart = new Date(year, month, 1, 0, 0, 0, 0);
|
||||
const rangeEnd = new Date(year, month + 1, 0, 23, 59, 59, 999);
|
||||
|
||||
return {
|
||||
year,
|
||||
month: month + 1,
|
||||
monthKey,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
};
|
||||
};
|
||||
|
||||
exports.distributeMonthlyPayouts = onSchedule(
|
||||
{
|
||||
@@ -40,64 +21,164 @@ exports.distributeMonthlyPayouts = onSchedule(
|
||||
async (event) => {
|
||||
const { scheduleTime } = event;
|
||||
const context = buildPreviousMonthContext(
|
||||
scheduleTime ? new Date(scheduleTime) : new Date()
|
||||
scheduleTime ? new Date(scheduleTime) : new Date(),
|
||||
);
|
||||
const monthKey = context.monthKey;
|
||||
const monthKey = buildMonthKey(context.rangeStart);
|
||||
const now = admin.firestore.Timestamp.now();
|
||||
|
||||
const statsSnapshot = await firestore
|
||||
.collectionGroup("monthly")
|
||||
const statsSnapshot = await db
|
||||
.collectionGroup("monthlyListens")
|
||||
.where("monthKey", "==", monthKey)
|
||||
.orderBy("streams", "desc")
|
||||
.limit(MAX_RECIPIENTS)
|
||||
.get();
|
||||
|
||||
const topEntries = statsSnapshot.docs
|
||||
.map((doc, index) => {
|
||||
const totalsDocRef = refList.projectStreamStatsMonthlyTotals.doc(monthKey);
|
||||
const totalsSnapshot = await totalsDocRef.get();
|
||||
|
||||
const entries = _.chain(statsSnapshot.docs)
|
||||
.map((doc) => {
|
||||
const data = doc.data() || {};
|
||||
return {
|
||||
rank: index + 1,
|
||||
projectId: data.projectId || doc.ref.parent.parent?.id || null,
|
||||
userId: data.userId || null,
|
||||
streams: typeof data.streams === "number" ? data.streams : 0,
|
||||
projectId:
|
||||
_.get(data, "projectId") || doc.ref.parent.parent?.id || null,
|
||||
userId: _.get(data, "userId", null),
|
||||
streams: _.toFinite(_.get(data, "streams", 0)),
|
||||
statsDocPath: doc.ref.path,
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry.projectId && entry.streams > 0);
|
||||
.filter((entry) => entry.projectId && entry.streams > 0)
|
||||
.orderBy(["streams"], ["desc"])
|
||||
.value();
|
||||
|
||||
const payoutPool = Number(
|
||||
(DISTRIBUTION_REVENUE_BASELINE * DISTRIBUTION_RATIO).toFixed(2)
|
||||
const payoutsTotalStreamsFromDocs = _.sumBy(entries, "streams");
|
||||
const totalsData = totalsSnapshot.exists ? totalsSnapshot.data() : null;
|
||||
let totalStreams = _.toFinite(_.get(totalsData, "totalStreams", 0));
|
||||
if (!totalStreams || totalStreams < payoutsTotalStreamsFromDocs) {
|
||||
totalStreams = payoutsTotalStreamsFromDocs;
|
||||
}
|
||||
|
||||
const payoutPool = _.round(
|
||||
DISTRIBUTION_REVENUE_BASELINE * DISTRIBUTION_RATIO,
|
||||
2,
|
||||
);
|
||||
|
||||
const weights = WEIGHTS.slice(0, topEntries.length);
|
||||
const weightSum = weights.reduce((sum, weight) => sum + weight, 0);
|
||||
|
||||
let allocations = topEntries.map((entry, idx) => {
|
||||
if (!weightSum) return 0;
|
||||
const rawAmount = (payoutPool * weights[idx]) / weightSum;
|
||||
return Number(rawAmount.toFixed(2));
|
||||
let allocations = _.map(entries, (entry) => {
|
||||
if (!totalStreams) return 0;
|
||||
const rawAmount = (payoutPool * entry.streams) / totalStreams;
|
||||
return _.round(rawAmount, 2);
|
||||
});
|
||||
|
||||
if (allocations.length > 0) {
|
||||
const allocatedTotal = Number(
|
||||
allocations.reduce((sum, amount) => sum + amount, 0).toFixed(2)
|
||||
);
|
||||
const remainder = Number((payoutPool - allocatedTotal).toFixed(2));
|
||||
const allocatedTotal = _.round(_.sum(allocations), 2);
|
||||
const remainder = _.round(payoutPool - allocatedTotal, 2);
|
||||
if (remainder !== 0) {
|
||||
allocations[0] = Number((allocations[0] + remainder).toFixed(2));
|
||||
allocations[0] = _.round(allocations[0] + remainder, 2);
|
||||
}
|
||||
}
|
||||
|
||||
const payouts = topEntries.map((entry, idx) => ({
|
||||
rank: entry.rank,
|
||||
const payouts = entries.map((entry, idx) => ({
|
||||
rank: idx + 1,
|
||||
projectId: entry.projectId,
|
||||
userId: entry.userId,
|
||||
streams: entry.streams,
|
||||
weight: weights[idx],
|
||||
share: totalStreams ? _.round(entry.streams / totalStreams, 6) : 0,
|
||||
amount: allocations[idx],
|
||||
statsDocPath: entry.statsDocPath,
|
||||
}));
|
||||
|
||||
const userDocs = _(payouts)
|
||||
.filter((payout) => !!payout.userId)
|
||||
.groupBy("userId")
|
||||
.map((projects, userId) => {
|
||||
const sortedProjects = _.orderBy(projects, ["amount"], ["desc"]).map(
|
||||
(project) => ({
|
||||
projectId: project.projectId,
|
||||
rank: project.rank,
|
||||
amount: project.amount,
|
||||
streams: project.streams,
|
||||
share: project.share,
|
||||
statsDocPath: project.statsDocPath,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
userId,
|
||||
totalAmount: _.round(_.sumBy(projects, "amount"), 2),
|
||||
totalStreams: _.sumBy(projects, "streams"),
|
||||
projects: sortedProjects,
|
||||
};
|
||||
})
|
||||
.value();
|
||||
|
||||
if (userDocs.length) {
|
||||
const docs = userDocs.map((userData) => {
|
||||
const docId = `${monthKey}_${userData.userId}`;
|
||||
return {
|
||||
ref: refList.monthlyPayoutEntries.doc(docId),
|
||||
data: {
|
||||
monthKey,
|
||||
userId: userData.userId,
|
||||
payoutMonth: context.month,
|
||||
totalAmount: userData.totalAmount,
|
||||
totalStreams: userData.totalStreams,
|
||||
projects: userData.projects,
|
||||
computedAt: now,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await batchFirestore({
|
||||
docs,
|
||||
type: BATCH_TYPE.UPDATE,
|
||||
});
|
||||
|
||||
const payoutLabel = `${String(context.month).padStart(2, "0")}/${
|
||||
context.year
|
||||
}`;
|
||||
await Promise.all(
|
||||
userDocs.map(async (userData) => {
|
||||
const receiverId =
|
||||
typeof userData.userId === "string" && userData.userId.trim()
|
||||
? userData.userId.trim()
|
||||
: null;
|
||||
const amount = Number(userData.totalAmount) || 0;
|
||||
if (!receiverId || amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
const amountLabel = amount.toFixed(2);
|
||||
const message = `Tes revenus de ${payoutLabel} (${amountLabel} €) sont disponibles.`;
|
||||
try {
|
||||
await sendNotification({
|
||||
sender: "SYSTEM",
|
||||
receiver: receiverId,
|
||||
receiverCollection: "users",
|
||||
title: "Revenus disponibles",
|
||||
message,
|
||||
data: {
|
||||
type: ALERT_TYPE?.PAYOUT_AVAILABLE,
|
||||
monthKey,
|
||||
month: context.month,
|
||||
year: context.year,
|
||||
totalAmount: amount,
|
||||
totalStreams: userData.totalStreams,
|
||||
projects: userData.projects,
|
||||
},
|
||||
});
|
||||
} catch (notifError) {
|
||||
console.log(
|
||||
"[distributeMonthlyPayouts] Failed to send payout notification:",
|
||||
{
|
||||
userId: receiverId,
|
||||
error: notifError?.message || String(notifError),
|
||||
},
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const summary = {
|
||||
monthKey,
|
||||
month: context.month,
|
||||
@@ -109,19 +190,16 @@ exports.distributeMonthlyPayouts = onSchedule(
|
||||
revenueBaseline: DISTRIBUTION_REVENUE_BASELINE,
|
||||
payoutRatio: DISTRIBUTION_RATIO,
|
||||
payoutPool,
|
||||
totalStreams: topEntries.reduce((sum, entry) => sum + entry.streams, 0),
|
||||
totalStreams,
|
||||
totalRecipients: payouts.length,
|
||||
totalAllocated: payouts.reduce(
|
||||
(sum, payout) => Number((sum + payout.amount).toFixed(2)),
|
||||
0
|
||||
),
|
||||
totalAllocated: _.round(_.sumBy(payouts, "amount"), 2),
|
||||
payouts,
|
||||
status: payouts.length ? "computed" : "no-data",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
computedAt: now,
|
||||
};
|
||||
|
||||
const docRef = firestore.collection("monthlyPayouts").doc(monthKey);
|
||||
const docRef = refList.monthlyPayouts.doc(monthKey);
|
||||
await docRef.set(summary, { merge: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
+90
-106
@@ -1,69 +1,12 @@
|
||||
const admin = require("firebase-admin");
|
||||
const { onDocumentWritten } = require("firebase-functions/firestore");
|
||||
const { refList } = require("../index");
|
||||
const _ = require("lodash");
|
||||
const { refList, db } = require("../index");
|
||||
const { getSunoTimestamps } = require("./lyrics");
|
||||
const { deleteFolder } = require("../helpers/firebase");
|
||||
const { buildMonthKey } = require("../helpers/stats");
|
||||
|
||||
const firestore = admin.firestore();
|
||||
|
||||
const buildMonthKey = (timestamp) => {
|
||||
const date = timestamp.toDate();
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
return `${year}-${month}`;
|
||||
};
|
||||
|
||||
exports.onProjectUpdate = onDocumentWritten(
|
||||
"projects/{projectId}",
|
||||
async (projectSnap) => {
|
||||
try {
|
||||
const { projectId } = projectSnap.params;
|
||||
|
||||
const currentData = projectSnap?.data?.after?.data() || null;
|
||||
const previousData = projectSnap?.data?.before?.data() || null;
|
||||
|
||||
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();
|
||||
} else {
|
||||
if (previousData) {
|
||||
// edit
|
||||
if (!previousData?.songUrl && currentData?.songUrl) {
|
||||
await refList.projects.doc(projectId).update({
|
||||
hasSong: true,
|
||||
});
|
||||
}
|
||||
if (!previousData?.playbackUrl && currentData?.playbackUrl) {
|
||||
await refList.projects.doc(projectId).update({
|
||||
hasPlayback: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
exports.onProjectViewsIncrement = onDocumentWritten(
|
||||
exports.onProjectWritten = onDocumentWritten(
|
||||
"projects/{projectId}",
|
||||
async (projectSnap) => {
|
||||
try {
|
||||
@@ -71,53 +14,94 @@ exports.onProjectViewsIncrement = onDocumentWritten(
|
||||
const beforeData = projectSnap?.data?.before?.data() || null;
|
||||
const afterData = projectSnap?.data?.after?.data() || null;
|
||||
|
||||
if (!afterData) return null;
|
||||
if (!afterData) {
|
||||
const { userId } = beforeData || {};
|
||||
|
||||
const beforeViews =
|
||||
typeof beforeData?.views === "number" && Number.isFinite(beforeData.views)
|
||||
? beforeData.views
|
||||
: 0;
|
||||
const afterViews =
|
||||
typeof afterData?.views === "number" && Number.isFinite(afterData.views)
|
||||
? afterData.views
|
||||
: 0;
|
||||
const delta = afterViews - beforeViews;
|
||||
|
||||
if (delta <= 0) return null;
|
||||
|
||||
const userId =
|
||||
typeof afterData?.userId === "string" && afterData.userId.trim().length
|
||||
? afterData.userId.trim()
|
||||
: null;
|
||||
const now = admin.firestore.Timestamp.now();
|
||||
const monthKey = buildMonthKey(now);
|
||||
|
||||
const statsDocRef = firestore
|
||||
.collection("projectStreamStats")
|
||||
.doc(projectId)
|
||||
.collection("monthly")
|
||||
.doc(monthKey);
|
||||
|
||||
await firestore.runTransaction(async (transaction) => {
|
||||
const statsSnapshot = await transaction.get(statsDocRef);
|
||||
const updatePayload = {
|
||||
projectId,
|
||||
userId,
|
||||
monthKey,
|
||||
updatedAt: now,
|
||||
lastStreamAt: now,
|
||||
lastDelta: delta,
|
||||
streams: admin.firestore.FieldValue.increment(delta),
|
||||
};
|
||||
|
||||
const hasFirstStream =
|
||||
statsSnapshot.exists && statsSnapshot.data()?.firstStreamAt;
|
||||
if (!hasFirstStream) {
|
||||
updatePayload.firstStreamAt = now;
|
||||
if (userId) {
|
||||
await deleteFolder(`users/${userId}/projects/${projectId}/`);
|
||||
}
|
||||
|
||||
transaction.set(statsDocRef, updatePayload, { merge: true });
|
||||
});
|
||||
const snapshot = await refList.tasks
|
||||
.where("projectId", "==", projectId)
|
||||
.get();
|
||||
if (!snapshot.empty) {
|
||||
const batch = db.batch();
|
||||
snapshot.forEach((doc) => {
|
||||
batch.delete(doc.ref);
|
||||
});
|
||||
await batch.commit();
|
||||
}
|
||||
|
||||
return null;
|
||||
} else if (!beforeData) {
|
||||
//song create
|
||||
} else {
|
||||
if (!beforeData?.songUrl && afterData?.songUrl) {
|
||||
await getSunoTimestamps(projectId);
|
||||
await refList.projects.doc(projectId).update({ hasSong: true });
|
||||
}
|
||||
|
||||
if (!beforeData?.playbackUrl && afterData?.playbackUrl) {
|
||||
await refList.projects.doc(projectId).update({ hasPlayback: true });
|
||||
}
|
||||
}
|
||||
|
||||
const beforeViews = _.toFinite(_.get(beforeData, "views", 0));
|
||||
const afterViews = _.toFinite(_.get(afterData, "views", 0));
|
||||
const delta = afterViews - beforeViews;
|
||||
|
||||
if (delta > 0) {
|
||||
const userIdRaw = _.get(afterData, "userId", null);
|
||||
const userId =
|
||||
_.isString(userIdRaw) && _.trim(userIdRaw).length
|
||||
? _.trim(userIdRaw)
|
||||
: null;
|
||||
const now = admin.firestore.Timestamp.now();
|
||||
const monthKey = buildMonthKey(now);
|
||||
|
||||
const statsDocRef = refList.projectStreamStats
|
||||
.doc(projectId)
|
||||
.collection("monthlyListens")
|
||||
.doc(monthKey);
|
||||
const totalsDocRef =
|
||||
refList.projectStreamStatsMonthlyTotals.doc(monthKey);
|
||||
|
||||
await db.runTransaction(async (transaction) => {
|
||||
const statsSnapshot = await transaction.get(statsDocRef);
|
||||
const totalsSnapshot = await transaction.get(totalsDocRef);
|
||||
const updatePayload = {
|
||||
projectId,
|
||||
userId,
|
||||
monthKey,
|
||||
updatedAt: now,
|
||||
lastStreamAt: now,
|
||||
lastDelta: delta,
|
||||
streams: admin.firestore.FieldValue.increment(delta),
|
||||
};
|
||||
|
||||
const hasFirstStream =
|
||||
statsSnapshot.exists && statsSnapshot.data()?.firstStreamAt;
|
||||
if (!hasFirstStream) {
|
||||
updatePayload.firstStreamAt = now;
|
||||
}
|
||||
|
||||
transaction.set(statsDocRef, updatePayload, { merge: true });
|
||||
|
||||
const totalsUpdatePayload = {
|
||||
monthKey,
|
||||
updatedAt: now,
|
||||
lastStreamAt: now,
|
||||
lastDelta: delta,
|
||||
totalStreams: admin.firestore.FieldValue.increment(delta),
|
||||
};
|
||||
const totalsHasFirstStream =
|
||||
totalsSnapshot.exists && totalsSnapshot.data()?.firstStreamAt;
|
||||
if (!totalsHasFirstStream) {
|
||||
totalsUpdatePayload.firstStreamAt = now;
|
||||
}
|
||||
transaction.set(totalsDocRef, totalsUpdatePayload, { merge: true });
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
@@ -126,5 +110,5 @@ exports.onProjectViewsIncrement = onDocumentWritten(
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ const fs = require("node:fs/promises");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const crypto = require("node:crypto");
|
||||
const { refList } = require("../index");
|
||||
|
||||
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
|
||||
|
||||
@@ -146,7 +147,7 @@ exports.generateVideoThumbnail = onObjectFinalized(
|
||||
const thumbnailUrl = `https://firebasestorage.googleapis.com/v0/b/${bucketName}/o/${encodedPath}?alt=media&token=${downloadToken}`;
|
||||
|
||||
if (projectId) {
|
||||
await admin.firestore().collection("projects").doc(projectId).set(
|
||||
await refList.projects.doc(projectId).set(
|
||||
{
|
||||
thumbnailUrl,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
|
||||
Generated
+21106
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,13 @@ const Input = ({
|
||||
});
|
||||
|
||||
const ContainerView = isBlur ? BlurView : View;
|
||||
const resolvedKeyboardType =
|
||||
textInputProps?.keyboardType ??
|
||||
(isNumeric
|
||||
? "numeric"
|
||||
: type === "email"
|
||||
? "email-address"
|
||||
: "default");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -176,13 +183,12 @@ const Input = ({
|
||||
: {})}
|
||||
{...(type === "email"
|
||||
? {
|
||||
keyboardType: "email-address",
|
||||
autoCapitalize: "none",
|
||||
autoCompleteType: "email",
|
||||
textContentType: "emailAddress",
|
||||
}
|
||||
: {})}
|
||||
keyboardType={isNumeric ? "numeric" : "default"}
|
||||
keyboardType={resolvedKeyboardType}
|
||||
keyboardAppearance="dark"
|
||||
inputAccessoryViewID={inputAccessoryViewID}
|
||||
{...textInputProps}
|
||||
|
||||
Reference in New Issue
Block a user