feat: remove publish to youtube

This commit is contained in:
2026-09-02 16:08:48 +02:00
parent 4e14a38d4e
commit f4549b694f
18 changed files with 15 additions and 804 deletions
-1
View File
@@ -57,6 +57,5 @@ exports.notifications = require('./src/notifications')
exports.rankings = require('./src/rankings')
exports.payouts = require('./src/payouts')
exports.subscription = require('./src/subscription')
exports.youtube = require('./src/youtube')
exports.orders = require('./src/orders')
exports.cron = require('./src/cron')
+3 -15
View File
@@ -18,7 +18,6 @@
"firebase-functions": "^6.0.1",
"fluent-ffmpeg": "^2.1.3",
"genkit": "^1.18.0",
"googleapis": "^131.0.0",
"lodash": "^4.17.21",
"resend": "^6.3.0",
"sharp": "^0.33.4",
@@ -4689,7 +4688,6 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
@@ -8885,24 +8883,12 @@
"node": ">=14"
}
},
"node_modules/googleapis": {
"version": "131.0.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-131.0.0.tgz",
"integrity": "sha512-fa4kdkY0VwHDw/04ItpQv2tlvlPIwbh6NjHDoWAVrV52GuaZbYCMOC5Y+hRmprp5HHIMRODmyb2YujlbZSRUbQ==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^9.0.0",
"googleapis-common": "^7.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/googleapis-common": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
"integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"extend": "^3.0.2",
"gaxios": "^6.0.3",
@@ -8924,6 +8910,7 @@
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"optional": true,
"bin": {
"uuid": "dist/bin/uuid"
}
@@ -13236,6 +13223,7 @@
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
"devOptional": true,
"license": "BSD"
},
"node_modules/util-deprecate": {
-1
View File
@@ -26,7 +26,6 @@
"firebase-functions": "^6.0.1",
"fluent-ffmpeg": "^2.1.3",
"genkit": "^1.18.0",
"googleapis": "^131.0.0",
"lodash": "^4.17.21",
"resend": "^6.3.0",
"sharp": "^0.33.4",
-294
View File
@@ -1,294 +0,0 @@
const { onCall, HttpsError } = require('firebase-functions/v2/https')
const { defineSecret } = require('firebase-functions/params')
const logger = require('firebase-functions/logger')
const functions = require('firebase-functions')
const admin = require('firebase-admin')
const { FieldValue } = require('firebase-admin/firestore')
const axios = require('axios')
const fs = require('node:fs')
const fsp = require('node:fs/promises')
const os = require('node:os')
const path = require('node:path')
const { google } = require('googleapis')
// ---- Secrets déclarés (gen2 + Secret Manager)
const S_YT_CLIENT_ID = defineSecret('YOUTUBE_CLIENT_ID')
const S_YT_CLIENT_SECRET = defineSecret('YOUTUBE_CLIENT_SECRET')
const S_YT_REFRESH_TOKEN = defineSecret('YOUTUBE_REFRESH_TOKEN')
const S_YT_REDIRECT_URI = defineSecret('YOUTUBE_REDIRECT_URI')
const S_YT_PRIVACY_STATUS = defineSecret('YOUTUBE_PRIVACY_STATUS')
const S_YT_CATEGORY_ID = defineSecret('YOUTUBE_CATEGORY_ID')
// Firestore
const firestore = admin.firestore()
const projectsRef = firestore.collection('projects')
const YOUTUBE_IN_PROGRESS_STATUSES = ['PUBLISHING', 'UPLOADING', 'PROCESSING', 'QUEUED']
// Lecture des secrets (recommandé en v2)
const getSecretsYoutubeConfig = () =>
Object.fromEntries(
Object.entries({
client_id: S_YT_CLIENT_ID.value(),
client_secret: S_YT_CLIENT_SECRET.value(),
refresh_token: S_YT_REFRESH_TOKEN.value(),
redirect_uri: S_YT_REDIRECT_URI.value(),
privacy_status: S_YT_PRIVACY_STATUS.value(),
category_id: S_YT_CATEGORY_ID.value(),
}).filter(([, value]) => value !== undefined && value !== '')
)
// Compat facultative v1 -> renverra {} en v2 (et on log un warn propre)
const getLegacyYoutubeConfig = () => {
if (typeof functions.config !== 'function') {
return {}
}
try {
return functions.config()?.youtube || {}
} catch (error) {
if (
typeof error?.message === 'string' &&
error.message.includes('functions.config() is no longer available')
) {
logger.warn(
'[publishPlaybackToYoutube] functions.config() indisponible, utilisation des secrets (Secret Manager)'
)
return {}
}
throw error
}
}
const ensureYoutubeConfig = () => {
// Fusionne (par prudence) lancienne config et les secrets actuels
const firebaseConfig = getLegacyYoutubeConfig()
const secretConfig = getSecretsYoutubeConfig()
const cfg = { ...firebaseConfig, ...secretConfig }
const requiredKeys = ['client_id', 'client_secret', 'refresh_token']
const missing = requiredKeys.filter((key) => !cfg[key])
if (missing.length) {
throw new HttpsError(
'failed-precondition',
`Configuration YouTube manquante: ${missing.join(', ')}`
)
}
return {
clientId: cfg.client_id,
clientSecret: cfg.client_secret,
refreshToken: cfg.refresh_token,
redirectUri: cfg.redirect_uri,
defaultPrivacyStatus: cfg.privacy_status,
defaultCategoryId: cfg.category_id,
}
}
const createYoutubeClient = ({ clientId, clientSecret, refreshToken, redirectUri }) => {
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri)
oauth2Client.setCredentials({ refresh_token: refreshToken })
const youtube = google.youtube({
version: 'v3',
auth: oauth2Client,
})
return { youtube, oauth2Client }
}
const downloadFile = async (url, destinationPath) => {
if (!/^https?:\/\//i.test(url || '')) {
throw new HttpsError('invalid-argument', `URL non valide: ${url}`)
}
await fsp.mkdir(path.dirname(destinationPath), { recursive: true })
const response = await axios.get(url, { responseType: 'stream' })
await new Promise((resolve, reject) => {
const writer = fs.createWriteStream(destinationPath)
response.data.pipe(writer)
writer.on('finish', resolve)
writer.on('error', reject)
})
return destinationPath
}
const buildVideoMetadata = (project, defaults) => {
const baseTitle = project?.title || 'Création MusicLand'
const youtubeTitle = `${baseTitle} | MusicLand`
const description = `Vidéo générée avec MusicLand pour ${baseTitle}. Rejoins l'aventure sur l'app MusicLand !`
const tags = Array.isArray(project?.youtubeTags)
? project.youtubeTags.filter(Boolean).slice(0, 500)
: undefined
const snippet = {
title: youtubeTitle,
description,
categoryId: defaults.defaultCategoryId,
}
if (tags && tags.length) {
snippet.tags = tags
}
return {
snippet,
status: {
privacyStatus: defaults.defaultPrivacyStatus,
embeddable: true,
selfDeclaredMadeForKids: false,
},
}
}
exports.publishPlaybackToYoutube = onCall(
{
timeoutSeconds: 540,
memory: '1GiB',
cors: [
'http://localhost:8081',
'https://musicland-one.vercel.app/',
'https://musicland-d33f9.firebaseapp.com',
],
// Secrets requis pour lexécution (v2)
secrets: [
S_YT_CLIENT_ID,
S_YT_CLIENT_SECRET,
S_YT_REFRESH_TOKEN,
S_YT_REDIRECT_URI,
S_YT_PRIVACY_STATUS,
S_YT_CATEGORY_ID,
],
},
async ({ data = {}, auth }) => {
const uid = auth?.uid
if (!uid) {
throw new HttpsError('unauthenticated', 'Authentification requise')
}
const projectId = data?.projectId
if (!projectId || typeof projectId !== 'string') {
throw new HttpsError('invalid-argument', 'Paramètre projectId requis')
}
const projectSnap = await projectsRef.doc(projectId).get()
if (!projectSnap.exists) {
throw new HttpsError('not-found', 'Projet introuvable')
}
const project = projectSnap.data()
if (!project || project.userId !== uid) {
throw new HttpsError('permission-denied', "Vous n'avez pas les droits sur ce projet")
}
if (!project.playbackUrl) {
throw new HttpsError('failed-precondition', 'Aucun playback disponible pour la publication')
}
if (project.youtubeStatus && YOUTUBE_IN_PROGRESS_STATUSES.includes(project.youtubeStatus)) {
throw new HttpsError(
'failed-precondition',
'Une publication est déjà en cours pour ce projet'
)
}
let youtubeDefaults
let youtubeClient
try {
youtubeDefaults = ensureYoutubeConfig()
youtubeClient = createYoutubeClient(youtubeDefaults)
await youtubeClient.oauth2Client.getAccessToken()
} catch (error) {
logger.error('[publishPlaybackToYoutube] configuration invalide', {
error: error?.message,
})
throw new HttpsError('failed-precondition', 'Configuration YouTube invalide ou incomplète')
}
const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'yt-upload-'))
const videoPath = path.join(tmpDir, 'playback.mp4')
try {
await projectsRef.doc(projectId).set(
{
youtubeStatus: 'PUBLISHING',
youtubePublished: false,
youtubeError: null,
updatedAt: FieldValue.serverTimestamp(),
},
{ merge: true }
)
await downloadFile(project.playbackUrl, videoPath)
const metadata = buildVideoMetadata(project, youtubeDefaults)
const uploadResponse = await youtubeClient.youtube.videos.insert({
part: ['snippet', 'status'].join(','),
requestBody: metadata,
media: {
body: fs.createReadStream(videoPath),
},
})
const videoId = uploadResponse?.data?.id
if (!videoId) {
throw new Error('ID de vidéo introuvable dans la réponse YouTube')
}
const youtubeLink = `https://www.youtube.com/watch?v=${videoId}`
await projectsRef.doc(projectId).set(
{
youtubeStatus: 'PUBLISHED',
youtubePublished: true,
youtubeUrl: youtubeLink,
youtubeVideoId: videoId,
youtubePublishedAt: FieldValue.serverTimestamp(),
youtubeError: null,
updatedAt: FieldValue.serverTimestamp(),
},
{ merge: true }
)
logger.info('[publishPlaybackToYoutube] publication réussie', {
projectId,
videoId,
})
return {
videoId,
youtubeUrl: youtubeLink,
}
} catch (error) {
logger.error('[publishPlaybackToYoutube] échec de publication', {
projectId,
error: error?.message,
})
const errorMessage =
error instanceof HttpsError
? error.message
: error?.message || 'Publication YouTube échouée'
await projectsRef.doc(projectId).set(
{
youtubeStatus: 'FAILED',
youtubePublished: false,
youtubeError: errorMessage,
updatedAt: FieldValue.serverTimestamp(),
},
{ merge: true }
)
if (error instanceof HttpsError) {
throw error
}
throw new HttpsError('internal', errorMessage)
} finally {
await fsp.rm(tmpDir, { recursive: true, force: true })
}
}
)