feat: remove publish to youtube
This commit is contained in:
@@ -57,6 +57,5 @@ exports.notifications = require('./src/notifications')
|
|||||||
exports.rankings = require('./src/rankings')
|
exports.rankings = require('./src/rankings')
|
||||||
exports.payouts = require('./src/payouts')
|
exports.payouts = require('./src/payouts')
|
||||||
exports.subscription = require('./src/subscription')
|
exports.subscription = require('./src/subscription')
|
||||||
exports.youtube = require('./src/youtube')
|
|
||||||
exports.orders = require('./src/orders')
|
exports.orders = require('./src/orders')
|
||||||
exports.cron = require('./src/cron')
|
exports.cron = require('./src/cron')
|
||||||
|
|||||||
Generated
+3
-15
@@ -18,7 +18,6 @@
|
|||||||
"firebase-functions": "^6.0.1",
|
"firebase-functions": "^6.0.1",
|
||||||
"fluent-ffmpeg": "^2.1.3",
|
"fluent-ffmpeg": "^2.1.3",
|
||||||
"genkit": "^1.18.0",
|
"genkit": "^1.18.0",
|
||||||
"googleapis": "^131.0.0",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"resend": "^6.3.0",
|
"resend": "^6.3.0",
|
||||||
"sharp": "^0.33.4",
|
"sharp": "^0.33.4",
|
||||||
@@ -4689,7 +4688,6 @@
|
|||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -8885,24 +8883,12 @@
|
|||||||
"node": ">=14"
|
"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": {
|
"node_modules/googleapis-common": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
|
||||||
"integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
|
"integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"extend": "^3.0.2",
|
"extend": "^3.0.2",
|
||||||
"gaxios": "^6.0.3",
|
"gaxios": "^6.0.3",
|
||||||
@@ -8924,6 +8910,7 @@
|
|||||||
"https://github.com/sponsors/ctavan"
|
"https://github.com/sponsors/ctavan"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
@@ -13236,6 +13223,7 @@
|
|||||||
"version": "2.0.8",
|
"version": "2.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
||||||
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
|
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "BSD"
|
"license": "BSD"
|
||||||
},
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
"firebase-functions": "^6.0.1",
|
"firebase-functions": "^6.0.1",
|
||||||
"fluent-ffmpeg": "^2.1.3",
|
"fluent-ffmpeg": "^2.1.3",
|
||||||
"genkit": "^1.18.0",
|
"genkit": "^1.18.0",
|
||||||
"googleapis": "^131.0.0",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"resend": "^6.3.0",
|
"resend": "^6.3.0",
|
||||||
"sharp": "^0.33.4",
|
"sharp": "^0.33.4",
|
||||||
|
|||||||
@@ -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) l’ancienne 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 l’exé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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -270,7 +270,6 @@ export const cardsImg = {
|
|||||||
writing: require('./icons/writing.png'),
|
writing: require('./icons/writing.png'),
|
||||||
studio: require('./icons/studio.png'),
|
studio: require('./icons/studio.png'),
|
||||||
video: require('./icons/video.png'),
|
video: require('./icons/video.png'),
|
||||||
production: require('./icons/production.png'),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const subBadges = {
|
export const subBadges = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { BlurView } from 'expo-blur'
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Animated, FlatList, Platform, StyleSheet, View, useWindowDimensions } from 'react-native'
|
import { Animated, FlatList, Platform, StyleSheet, View, useWindowDimensions } from 'react-native'
|
||||||
import { responsiveHeight } from '../../actions/responsiveSizes'
|
import { responsiveHeight } from '../../actions/responsiveSizes'
|
||||||
import { ai, cardsImg } from '../../assets'
|
import { ai } from '../../assets'
|
||||||
import { gutters } from '../../styles'
|
import { gutters } from '../../styles'
|
||||||
import { getCreationStageStates } from '../../utils/projectStages'
|
import { getCreationStageStates } from '../../utils/projectStages'
|
||||||
import AnimatedPaginationDot from '../AnimatedPaginationDot/AnimatedPaginationDot'
|
import AnimatedPaginationDot from '../AnimatedPaginationDot/AnimatedPaginationDot'
|
||||||
@@ -27,12 +27,6 @@ const STAGE_CARD_CONTENT = [
|
|||||||
description: "Theo t'accompagne pour créer ton playback.",
|
description: "Theo t'accompagne pour créer ton playback.",
|
||||||
image: ai.rightIcon,
|
image: ai.rightIcon,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'publisher',
|
|
||||||
title: 'Publication',
|
|
||||||
description: 'Ta vidéo est prête ? Direction YouTube !',
|
|
||||||
image: cardsImg.production,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const WEB_SCROLL_INACTIVE_DELTA = 0.05
|
const WEB_SCROLL_INACTIVE_DELTA = 0.05
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { ai, cardsImg } from '../../assets'
|
import { ai } from '../../assets'
|
||||||
import { getCreationStageStates } from '../../utils/projectStages'
|
import { getCreationStageStates } from '../../utils/projectStages'
|
||||||
import AnimatedPaginationDot from '../AnimatedPaginationDot/AnimatedPaginationDot'
|
import AnimatedPaginationDot from '../AnimatedPaginationDot/AnimatedPaginationDot'
|
||||||
import PersonaCard from '../cards/PersonaCard/PersonaCard'
|
import PersonaCard from '../cards/PersonaCard/PersonaCard'
|
||||||
@@ -33,12 +33,6 @@ const STAGE_CARD_CONTENT = [
|
|||||||
description: "Theo t'accompagne pour créer ton playback.",
|
description: "Theo t'accompagne pour créer ton playback.",
|
||||||
image: ai.rightIcon,
|
image: ai.rightIcon,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'publisher',
|
|
||||||
title: 'Publication',
|
|
||||||
description: 'Ta vidéo est prête ? Direction YouTube !',
|
|
||||||
image: cardsImg.production,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const WEB_SCROLL_INACTIVE_DELTA = 0.05
|
const WEB_SCROLL_INACTIVE_DELTA = 0.05
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import PlaybackOnboarding from '../screens/Playback/PlaybackOnboarding'
|
|||||||
import RecordPlayback from '../screens/Playback/RecordPlayback'
|
import RecordPlayback from '../screens/Playback/RecordPlayback'
|
||||||
import RecordedPlayback from '../screens/Playback/RecordedPlayback'
|
import RecordedPlayback from '../screens/Playback/RecordedPlayback'
|
||||||
import VideoFinalize from '../screens/Playback/VideoFinalize'
|
import VideoFinalize from '../screens/Playback/VideoFinalize'
|
||||||
import PublishYoutube from '../screens/Publishing/PublishYoutube'
|
|
||||||
import PrivacyPolicy from '../screens/PrivacyPolicy'
|
import PrivacyPolicy from '../screens/PrivacyPolicy'
|
||||||
import Subscriptions from '../screens/Subscriptions'
|
import Subscriptions from '../screens/Subscriptions'
|
||||||
import DownloadPrices from '../screens/Production/DownloadPrices'
|
import DownloadPrices from '../screens/Production/DownloadPrices'
|
||||||
@@ -265,10 +264,6 @@ const baseScreens = [
|
|||||||
name: Routes.VideoFinalize,
|
name: Routes.VideoFinalize,
|
||||||
component: VideoFinalize,
|
component: VideoFinalize,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: Routes.PublishYoutube,
|
|
||||||
component: PublishYoutube,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: Routes.AllMyPlaylist,
|
name: Routes.AllMyPlaylist,
|
||||||
component: AllMyPlaylist,
|
component: AllMyPlaylist,
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ export const Routes = {
|
|||||||
ChooseDecor: 'ChooseDecor',
|
ChooseDecor: 'ChooseDecor',
|
||||||
CreatingDecor: 'CreatingDecor',
|
CreatingDecor: 'CreatingDecor',
|
||||||
VideoFinalize: 'VideoFinalize',
|
VideoFinalize: 'VideoFinalize',
|
||||||
PublishYoutube: 'PublishYoutube',
|
|
||||||
|
|
||||||
Create: 'Create',
|
Create: 'Create',
|
||||||
HitParade: 'HitParade',
|
HitParade: 'HitParade',
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ const HIDDEN_ROUTE_NAMES = new Set([
|
|||||||
Routes.ChooseDecor,
|
Routes.ChooseDecor,
|
||||||
Routes.CreatingDecor,
|
Routes.CreatingDecor,
|
||||||
Routes.VideoFinalize,
|
Routes.VideoFinalize,
|
||||||
Routes.PublishYoutube,
|
|
||||||
Routes.FlowSelection,
|
Routes.FlowSelection,
|
||||||
Routes.ChooseCoverType,
|
Routes.ChooseCoverType,
|
||||||
Routes.Settings,
|
Routes.Settings,
|
||||||
|
|||||||
@@ -72,16 +72,6 @@ const STAGE_CARD_CONTENT = [
|
|||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
lockSide: 'right',
|
lockSide: 'right',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'publisher',
|
|
||||||
step: 'ÉTAPE 4',
|
|
||||||
description:
|
|
||||||
'Je suis Mr Benhaï, Producteur de MusicLand, et je vais te faire une proposition qui pourrait t’intéresser. On se retrouve à la sortie du studio !',
|
|
||||||
image: cardsImg.production,
|
|
||||||
imagePosition: 'right',
|
|
||||||
textAlign: 'right',
|
|
||||||
lockSide: 'left',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const ACTIVE_SUBSCRIPTION_STATUSES = new Set(['trialing', 'active', 'past_due', 'unpaid'])
|
const ACTIVE_SUBSCRIPTION_STATUSES = new Set(['trialing', 'active', 'past_due', 'unpaid'])
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import FontAwesome from '@expo/vector-icons/FontAwesome'
|
|||||||
import { BlurView } from 'expo-blur'
|
import { BlurView } from 'expo-blur'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { Image, Platform, Pressable, Text, View } from 'react-native'
|
import { Image, Platform, Pressable, Text, View } from 'react-native'
|
||||||
import { ai, background, cardsImg } from '../assets'
|
import { ai, background } from '../assets'
|
||||||
import Page from '../layouts/Page'
|
import Page from '../layouts/Page'
|
||||||
import { navigate } from '../navigation/NavigationService'
|
import { navigate } from '../navigation/NavigationService'
|
||||||
import { useUserData } from '../providers/UserDataProvider'
|
import { useUserData } from '../providers/UserDataProvider'
|
||||||
@@ -36,14 +36,6 @@ const CREATE_DATA = [
|
|||||||
desc: "Theo t'accompagne pour créer ton playback.",
|
desc: "Theo t'accompagne pour créer ton playback.",
|
||||||
type: 'Director',
|
type: 'Director',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
stageKey: 'publisher',
|
|
||||||
img: cardsImg.production,
|
|
||||||
bg: background.productionBG2,
|
|
||||||
label: 'Publication',
|
|
||||||
desc: 'Publions ta vidéo sur YouTube !',
|
|
||||||
type: 'Producteur',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const NewMusicOptions = () => {
|
const NewMusicOptions = () => {
|
||||||
|
|||||||
@@ -407,7 +407,11 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
{ merge: true }
|
{ merge: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
navigate(Routes.PublishYoutube, { projectId: projectForDownload.id })
|
setTooltip({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Playback publié sur MusicLand !',
|
||||||
|
})
|
||||||
|
navigate(Routes.Home)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setTooltip({
|
setTooltip({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const StreamSong = () => {
|
|||||||
if (!hasAcceptedPublication) {
|
if (!hasAcceptedPublication) {
|
||||||
setTooltip({
|
setTooltip({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: 'Confirme la diffusion sur Musicland et YouTube avant de publier',
|
text: 'Confirme la diffusion sur MusicLand avant de publier',
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ const StreamSong = () => {
|
|||||||
<AppCheckbox
|
<AppCheckbox
|
||||||
selected={hasAcceptedPublication}
|
selected={hasAcceptedPublication}
|
||||||
onPress={() => setHasAcceptedPublication((prevState) => !prevState)}
|
onPress={() => setHasAcceptedPublication((prevState) => !prevState)}
|
||||||
label="J'accepte la diffusion de mon contenu sur Musicland et YouTube."
|
label="J'accepte la diffusion de mon contenu sur MusicLand."
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,327 +0,0 @@
|
|||||||
import { useRoute } from '@react-navigation/native'
|
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
|
||||||
import { Linking, StyleSheet, Text, View } from 'react-native'
|
|
||||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
|
||||||
import { background } from '../../assets'
|
|
||||||
import AppCheckbox from '../../components/AppCheckbox'
|
|
||||||
import BackgroundVideo from '../../components/BackgroundVideo'
|
|
||||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
|
||||||
import GradientButton from '../../components/GradientButton'
|
|
||||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
|
||||||
import alert from '../../components/Alert'
|
|
||||||
import firebase, { projectsRef, serverTimestamp } from '../../config/firebase'
|
|
||||||
import Page from '../../layouts/Page'
|
|
||||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
|
||||||
import { Routes } from '../../navigation'
|
|
||||||
import { useUser } from '../../providers/UserDataProvider'
|
|
||||||
import { Palette, gutters } from '../../styles'
|
|
||||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
|
||||||
import { isWeb } from '../../hooks/useLayoutType'
|
|
||||||
|
|
||||||
const PublishYoutube = () => {
|
|
||||||
const route = useRoute()
|
|
||||||
const routeProjectId = route?.params?.projectId ?? null
|
|
||||||
const { userProjects = [], selectedProject, videos } = useUser()
|
|
||||||
const { setTooltip } = useMinuit()
|
|
||||||
|
|
||||||
const project = useMemo(() => {
|
|
||||||
if (routeProjectId) {
|
|
||||||
return userProjects.find((item) => item?.id === routeProjectId) || selectedProject || null
|
|
||||||
}
|
|
||||||
if (selectedProject?.id) {
|
|
||||||
return selectedProject
|
|
||||||
}
|
|
||||||
return userProjects.length ? userProjects[0] : null
|
|
||||||
}, [routeProjectId, selectedProject, userProjects])
|
|
||||||
|
|
||||||
const projectId = project?.id || routeProjectId || null
|
|
||||||
const introVideo = isWeb ? videos?.benhaiWeb : videos?.benhai
|
|
||||||
|
|
||||||
const [saving, setSaving] = useState(false)
|
|
||||||
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
|
|
||||||
const youtubeUrl = project?.youtubeUrl ?? null
|
|
||||||
const youtubeStatus = project?.youtubeStatus ?? null
|
|
||||||
const youtubeError = project?.youtubeError ?? null
|
|
||||||
const playbackReady = !!project?.playbackUrl
|
|
||||||
const hasYoutubePublication = !!youtubeUrl
|
|
||||||
const isPublishing = useMemo(
|
|
||||||
() => ['PUBLISHING', 'UPLOADING', 'PROCESSING', 'QUEUED'].includes(youtubeStatus),
|
|
||||||
[youtubeStatus]
|
|
||||||
)
|
|
||||||
const publishDisabled =
|
|
||||||
saving || isPublishing || !playbackReady || !projectId || !hasAcceptedPublication
|
|
||||||
const publishButtonLabel = isPublishing
|
|
||||||
? 'Publication en cours...'
|
|
||||||
: hasYoutubePublication
|
|
||||||
? 'Mettre à jour sur YouTube'
|
|
||||||
: 'Publier sur YouTube'
|
|
||||||
const statusMessage = useMemo(() => {
|
|
||||||
if (!playbackReady) {
|
|
||||||
return 'Termine la création de ton playback avec Theo pour débloquer la publication YouTube.'
|
|
||||||
}
|
|
||||||
if (isPublishing) {
|
|
||||||
return 'La publication de ta vidéo est en cours... Patiente un instant.'
|
|
||||||
}
|
|
||||||
if (youtubeError || youtubeStatus === 'FAILED') {
|
|
||||||
return 'La dernière tentative de publication a échoué. Tu peux réessayer ci-dessous.'
|
|
||||||
}
|
|
||||||
if (hasYoutubePublication) {
|
|
||||||
return 'Ta vidéo est publiée sur la chaîne YouTube MusicLand. Tu peux la partager dès maintenant.'
|
|
||||||
}
|
|
||||||
return 'Partage ton playback sur la chaîne YouTube MusicLand.'
|
|
||||||
}, [hasYoutubePublication, isPublishing, playbackReady, youtubeError, youtubeStatus])
|
|
||||||
|
|
||||||
const handleOpenYoutube = useCallback(async () => {
|
|
||||||
if (!youtubeUrl) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await Linking.openURL(youtubeUrl)
|
|
||||||
} catch (_error) {
|
|
||||||
setTooltip?.({
|
|
||||||
type: 'error',
|
|
||||||
text: "Impossible d'ouvrir le lien YouTube",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [setTooltip, youtubeUrl])
|
|
||||||
|
|
||||||
const handleMarkPublished = useCallback(async () => {
|
|
||||||
if (!projectId) {
|
|
||||||
setTooltip?.({
|
|
||||||
type: 'error',
|
|
||||||
text: 'Sélectionnez un projet avant de publier',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!playbackReady) {
|
|
||||||
setTooltip?.({
|
|
||||||
type: 'error',
|
|
||||||
text: "Aucun playback n'est disponible pour la publication",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSaving(true)
|
|
||||||
try {
|
|
||||||
// publication sur youtube en standby
|
|
||||||
// const publishCallable = firebase.functions().httpsCallable('youtube-publishPlaybackToYoutube')
|
|
||||||
// await publishCallable({ projectId })
|
|
||||||
setTooltip?.({
|
|
||||||
type: 'success',
|
|
||||||
text: 'Publication lancée sur YouTube',
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
setTooltip?.({
|
|
||||||
type: 'error',
|
|
||||||
text: error?.message || 'Impossible de lancer la publication',
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}, [playbackReady, projectId, setTooltip])
|
|
||||||
|
|
||||||
const handleConfirmPublish = useCallback(() => {
|
|
||||||
if (!hasAcceptedPublication) {
|
|
||||||
setTooltip?.({
|
|
||||||
type: 'error',
|
|
||||||
text: 'Confirme la diffusion sur YouTube avant de publier',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (publishDisabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const confirmTitle = hasYoutubePublication
|
|
||||||
? 'Confirmer la mise à jour'
|
|
||||||
: 'Confirmer la publication'
|
|
||||||
const confirmDescription = hasYoutubePublication
|
|
||||||
? 'La vidéo existe déjà sur YouTube. Confirme que tu souhaites remplacer la version publiée.'
|
|
||||||
: 'Confirme que tu souhaites publier ce playback sur YouTube. Il sera visible publiquement.'
|
|
||||||
alert(confirmTitle, confirmDescription, [
|
|
||||||
{ text: 'Annuler', style: 'cancel' },
|
|
||||||
{
|
|
||||||
text: hasYoutubePublication ? 'Mettre à jour' : 'Je confirme',
|
|
||||||
onPress: handleMarkPublished,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}, [
|
|
||||||
handleMarkPublished,
|
|
||||||
hasAcceptedPublication,
|
|
||||||
hasYoutubePublication,
|
|
||||||
publishDisabled,
|
|
||||||
setTooltip,
|
|
||||||
])
|
|
||||||
|
|
||||||
const handleResetPublication = useCallback(async () => {
|
|
||||||
if (!projectId) {
|
|
||||||
setTooltip?.({
|
|
||||||
type: 'error',
|
|
||||||
text: 'Sélectionnez un projet avant de réinitialiser',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSaving(true)
|
|
||||||
try {
|
|
||||||
await projectsRef.doc(projectId).set(
|
|
||||||
{
|
|
||||||
youtubePublished: false,
|
|
||||||
youtubeUrl: null,
|
|
||||||
youtubeVideoId: null,
|
|
||||||
youtubePublishedAt: null,
|
|
||||||
youtubeStatus: 'IDLE',
|
|
||||||
youtubeError: null,
|
|
||||||
updatedAt: serverTimestamp(),
|
|
||||||
},
|
|
||||||
{ merge: true }
|
|
||||||
)
|
|
||||||
setTooltip?.({
|
|
||||||
type: 'success',
|
|
||||||
text: 'Publication YouTube réinitialisée',
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
setTooltip?.({
|
|
||||||
type: 'error',
|
|
||||||
text: error?.message || 'Réinitialisation impossible',
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}, [projectId, setTooltip])
|
|
||||||
|
|
||||||
const handleGoHome = useCallback(() => {
|
|
||||||
navigate(Routes.BottomTab, {
|
|
||||||
screen: Routes.HomeStack,
|
|
||||||
params: {
|
|
||||||
screen: Routes.Home,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page
|
|
||||||
backgroundImg={background.playbackBG2}
|
|
||||||
backgroundContent={
|
|
||||||
<BackgroundVideo
|
|
||||||
source={introVideo}
|
|
||||||
soundButtonStyle={isWeb ? styles.rightSideControl : undefined}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
containerStyle={isWeb ? styles.videoPage : undefined}
|
|
||||||
headerType="NONE"
|
|
||||||
>
|
|
||||||
<MusicLandHeader progress={100} onPressBack={goBack} />
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
paddingHorizontal: gutters,
|
|
||||||
paddingBottom: gutters * 2,
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={{ alignItems: 'center', gap: 18 }}>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 22,
|
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
||||||
color: Palette.white,
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{hasYoutubePublication ? 'Ta vidéo est déjà sur YouTube' : 'Publier sur YouTube'}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
color: Palette.white,
|
|
||||||
opacity: 0.9,
|
|
||||||
textAlign: 'center',
|
|
||||||
lineHeight: 20,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{statusMessage}
|
|
||||||
</Text>
|
|
||||||
{youtubeError && (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
color: Palette.red,
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{youtubeError}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{hasYoutubePublication && youtubeUrl && (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
fontFamily: FONT_FAMILY.InterMedium,
|
|
||||||
color: Palette.white,
|
|
||||||
textAlign: 'center',
|
|
||||||
opacity: 0.8,
|
|
||||||
}}
|
|
||||||
numberOfLines={2}
|
|
||||||
>
|
|
||||||
{youtubeUrl}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View style={{ gap: 14 }}>
|
|
||||||
<View style={styles.consentContainer}>
|
|
||||||
<AppCheckbox
|
|
||||||
selected={hasAcceptedPublication}
|
|
||||||
onPress={() => setHasAcceptedPublication((prevState) => !prevState)}
|
|
||||||
label="J'accepte la diffusion de mon contenu sur YouTube."
|
|
||||||
/>
|
|
||||||
<Text style={styles.consentDescription}>
|
|
||||||
Cette confirmation est requise avant toute publication sur YouTube.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<GradientButton
|
|
||||||
title={publishButtonLabel}
|
|
||||||
onPress={handleConfirmPublish}
|
|
||||||
disabled={publishDisabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{hasYoutubePublication && youtubeUrl && (
|
|
||||||
<BorderGradientButton
|
|
||||||
title="Voir la vidéo sur YouTube"
|
|
||||||
onPress={handleOpenYoutube}
|
|
||||||
disabled={saving || isPublishing}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{hasYoutubePublication && (
|
|
||||||
<BorderGradientButton
|
|
||||||
title="Réinitialiser la publication"
|
|
||||||
onPress={handleResetPublication}
|
|
||||||
disabled={saving || isPublishing}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<BorderGradientButton title="Retour à l'accueil" onPress={handleGoHome} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
videoPage: {
|
|
||||||
alignSelf: 'center',
|
|
||||||
},
|
|
||||||
rightSideControl: {
|
|
||||||
right: 24,
|
|
||||||
left: 'auto',
|
|
||||||
},
|
|
||||||
consentContainer: {
|
|
||||||
gap: 6,
|
|
||||||
},
|
|
||||||
consentDescription: {
|
|
||||||
fontSize: 13,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
color: Palette.white,
|
|
||||||
opacity: 0.8,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default PublishYoutube
|
|
||||||
@@ -437,7 +437,7 @@ const ShareAndCreditsModal = ({
|
|||||||
<AppCheckbox
|
<AppCheckbox
|
||||||
selected={hasAcceptedPublication}
|
selected={hasAcceptedPublication}
|
||||||
onPress={() => setHasAcceptedPublication((prev) => !prev)}
|
onPress={() => setHasAcceptedPublication((prev) => !prev)}
|
||||||
label="J'accepte de diffuser mon contenu sur la plateforme de streaming de musicland et sur youtube"
|
label="J'accepte de diffuser mon contenu sur la plateforme de streaming de MusicLand"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Routes } from '../navigation/Routes'
|
import { Routes } from '../navigation/Routes'
|
||||||
|
|
||||||
export const CREATION_STAGE_KEYS = ['songwriter', 'beatmaker', 'director', 'publisher']
|
export const CREATION_STAGE_KEYS = ['songwriter', 'beatmaker', 'director']
|
||||||
|
|
||||||
const getSectionLyrics = (section) => {
|
const getSectionLyrics = (section) => {
|
||||||
if (typeof section === 'string') {
|
if (typeof section === 'string') {
|
||||||
@@ -36,13 +36,6 @@ const getStageMetadata = (project) => {
|
|||||||
const hasCoverUrl = !!project?.coverUrl
|
const hasCoverUrl = !!project?.coverUrl
|
||||||
const hasPlaybackAsset = !!project?.playbackUrl
|
const hasPlaybackAsset = !!project?.playbackUrl
|
||||||
const hasPlayback = hasPlaybackAsset || hasSongUrl
|
const hasPlayback = hasPlaybackAsset || hasSongUrl
|
||||||
const youtubeUrl = project?.youtubeUrl ?? null
|
|
||||||
const youtubeStatus = project?.youtubeStatus ?? null
|
|
||||||
const youtubeError = project?.youtubeError ?? null
|
|
||||||
const hasYoutubePublication = !!youtubeUrl
|
|
||||||
const isYoutubePublishing = ['PUBLISHING', 'UPLOADING', 'PROCESSING', 'QUEUED'].includes(
|
|
||||||
youtubeStatus
|
|
||||||
)
|
|
||||||
const musicUrls = Array.isArray(project?.musicUrls) ? project.musicUrls.filter(Boolean) : []
|
const musicUrls = Array.isArray(project?.musicUrls) ? project.musicUrls.filter(Boolean) : []
|
||||||
const musicStatus = project?.musicStatus || null
|
const musicStatus = project?.musicStatus || null
|
||||||
const coverStatus = project?.coverStatus || null
|
const coverStatus = project?.coverStatus || null
|
||||||
@@ -65,13 +58,9 @@ const getStageMetadata = (project) => {
|
|||||||
coverStatus,
|
coverStatus,
|
||||||
playbackStatus,
|
playbackStatus,
|
||||||
isPlaybackDraftReady,
|
isPlaybackDraftReady,
|
||||||
youtubeStatus,
|
|
||||||
youtubeError,
|
|
||||||
isMusicGenerating: musicStatus === 'GENERATING',
|
isMusicGenerating: musicStatus === 'GENERATING',
|
||||||
isCoverGenerating: coverStatus === 'GENERATING',
|
isCoverGenerating: coverStatus === 'GENERATING',
|
||||||
isPlaybackGenerating,
|
isPlaybackGenerating,
|
||||||
hasYoutubePublication,
|
|
||||||
isYoutubePublishing,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +78,6 @@ const getStageLockState = (key, metadata) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return !metadata.hasCover
|
return !metadata.hasCover
|
||||||
case 'publisher':
|
|
||||||
return !metadata.hasPlaybackAsset
|
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -151,20 +138,6 @@ const getStageDescription = (key, metadata) => {
|
|||||||
return 'Modifier le playback généré'
|
return 'Modifier le playback généré'
|
||||||
}
|
}
|
||||||
return 'Commencer la création de votre playback'
|
return 'Commencer la création de votre playback'
|
||||||
case 'publisher':
|
|
||||||
if (!hasPlaybackAsset) {
|
|
||||||
return 'Créez un playback pour débloquer la publication'
|
|
||||||
}
|
|
||||||
if (metadata.isYoutubePublishing) {
|
|
||||||
return 'Publication de votre vidéo en cours'
|
|
||||||
}
|
|
||||||
if (metadata.youtubeError) {
|
|
||||||
return 'La publication a échoué, réessayez.'
|
|
||||||
}
|
|
||||||
if (metadata.hasYoutubePublication) {
|
|
||||||
return 'Votre vidéo est en ligne et prête à être partagée'
|
|
||||||
}
|
|
||||||
return 'Publier votre vidéo sur la chaîne YouTube MusicLand'
|
|
||||||
default:
|
default:
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -181,11 +154,6 @@ const getStageLockedDescription = (key, metadata) => {
|
|||||||
return metadata.lyricsCount > 0 ? undefined : 'Créez vos paroles pour débloquer le studio'
|
return metadata.lyricsCount > 0 ? undefined : 'Créez vos paroles pour débloquer le studio'
|
||||||
case 'director':
|
case 'director':
|
||||||
return metadata.hasPlaybackAsset ? 'Impossible de modifier le playback' : undefined
|
return metadata.hasPlaybackAsset ? 'Impossible de modifier le playback' : undefined
|
||||||
case 'publisher':
|
|
||||||
if (!metadata.hasPlaybackAsset) {
|
|
||||||
return 'Générez un playback pour débloquer la publication'
|
|
||||||
}
|
|
||||||
return metadata.hasYoutubePublication ? 'La vidéo est déjà publiée' : undefined
|
|
||||||
default:
|
default:
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -199,8 +167,6 @@ const getStageCompletionState = (key, metadata) => {
|
|||||||
return metadata.hasSongUrl || metadata.musicStatus === 'GENERATED'
|
return metadata.hasSongUrl || metadata.musicStatus === 'GENERATED'
|
||||||
case 'director':
|
case 'director':
|
||||||
return metadata.hasPlaybackAsset
|
return metadata.hasPlaybackAsset
|
||||||
case 'publisher':
|
|
||||||
return metadata.hasYoutubePublication
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -268,15 +234,6 @@ export const getStageAction = (key, project) => {
|
|||||||
route: Routes.Playback,
|
route: Routes.Playback,
|
||||||
params: project ? { project } : undefined,
|
params: project ? { project } : undefined,
|
||||||
}
|
}
|
||||||
case 'publisher': {
|
|
||||||
if (!project?.id) {
|
|
||||||
return { route: Routes.PublishYoutube }
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
route: Routes.PublishYoutube,
|
|
||||||
params: { projectId: project.id },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|||||||
-77
@@ -1,77 +0,0 @@
|
|||||||
# Publication YouTube – guide refresh token
|
|
||||||
|
|
||||||
## 1. Récupérer un code d'autorisation
|
|
||||||
|
|
||||||
### 1.1 Trouver ou créer le Client ID
|
|
||||||
|
|
||||||
1. Ouvre la console Google Cloud : <https://console.cloud.google.com/apis/credentials>
|
|
||||||
2. Sélectionne le projet lié au compte YouTube.
|
|
||||||
3. Dans l’onglet « Identifiants », repère l’ID client OAuth 2.0 existant au format :
|
|
||||||
```
|
|
||||||
123456789012-abcdefghijklmnopqrstu.apps.googleusercontent.com
|
|
||||||
```
|
|
||||||
Copie-le : c’est `YOUR_CLIENT_ID`.
|
|
||||||
4. S’il n’existe pas encore, clique sur « Créer des identifiants » → « ID client OAuth » → type « Application Web » et ajoute `http://localhost:8081` dans les URIs de redirection autorisées. Enregistre, puis note l’ID client et le secret.
|
|
||||||
|
|
||||||
### 1.2 Générer un code d'autorisation
|
|
||||||
|
|
||||||
1. Ouvre cette URL dans un navigateur (mets à jour `redirect_uri` si besoin) :
|
|
||||||
```
|
|
||||||
https://accounts.google.com/o/oauth2/v2/auth?client_id=305598753437-3mn43cs1phacao2f1ctg1dbur7rct0bt.apps.googleusercontent.com&redirect_uri=http://localhost:8081&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyoutube.upload&access_type=offline&prompt=consent
|
|
||||||
```
|
|
||||||
2. Connecte-toi au compte YouTube cible et accepte les permissions.
|
|
||||||
3. Google te redirige vers `http://localhost:8081/?code=...`. Copie la valeur du paramètre `code`.
|
|
||||||
|
|
||||||
## 2. Échanger le code contre un refresh token
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST https://oauth2.googleapis.com/token \
|
|
||||||
-d client_id=YOUR_CLIENT_ID \
|
|
||||||
-d client_secret=YOUR_CLIENT_SECRET \
|
|
||||||
-d code="CODE_RECU" \
|
|
||||||
-d grant_type=authorization_code \
|
|
||||||
-d redirect_uri=http://localhost:8081
|
|
||||||
```
|
|
||||||
|
|
||||||
La réponse JSON contient :
|
|
||||||
|
|
||||||
- `access_token` (jeton court terme),
|
|
||||||
- `refresh_token` (à conserver),
|
|
||||||
- `expires_in`, `scope`, `token_type`.
|
|
||||||
|
|
||||||
⚠️ Le `refresh_token` n’est renvoyé qu’une seule fois avec `access_type=offline`. Garde-le de manière sécurisée.
|
|
||||||
|
|
||||||
## 3. Mettre à jour les variables d'environnement Firebase
|
|
||||||
|
|
||||||
1. Enregistre les secrets côté Firebase (adapter le projet via `-P` si besoin) :
|
|
||||||
```bash
|
|
||||||
firebase functions:config:set \
|
|
||||||
YOUTUBE_CLIENT_ID="YOUR_CLIENT_ID.apps.googleusercontent.com" \
|
|
||||||
YOUTUBE_CLIENT_SECRET="YOUR_CLIENT_SECRET" \
|
|
||||||
YOUTUBE_REFRESH_TOKEN="YOUR_REFRESH_TOKEN"
|
|
||||||
YOUTUBE_REDIRECT_URI="http://localhost:8081" \
|
|
||||||
YOUTUBE_PRIVACY_STATUS="public" \
|
|
||||||
YOUTUBE_CATEGORY_ID="10" # 10 = Catégorie youtube de musique
|
|
||||||
```
|
|
||||||
2. Vérifie ce qui est stocké :
|
|
||||||
```bash
|
|
||||||
firebase functions:config:get
|
|
||||||
```
|
|
||||||
3. Redéploie la fonction :
|
|
||||||
```bash
|
|
||||||
firebase deploy --only functions:youtube
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Exemple de réponse OAuth
|
|
||||||
|
|
||||||
```
|
|
||||||
http://localhost:8081/?code=4/0Ab32j90LMDeJDShWSk1OkXwgbjrHS-qQpyYTlXhH9q3nlV0EJa6cTL-7nI9yOKHoz6rgRA&scope=https://www.googleapis.com/auth/youtube.upload
|
|
||||||
|
|
||||||
{
|
|
||||||
"access_token": "ya29.a0ATi6K2uR4DhnmTDl0EN2tvDogwwEwl8SDJLpUzvnZ8LGgOYjgmhqg7MoMM0_iTJR_NBEDPe2LlJDqCFFnIi9s3qRv_zfw_NeTnN-LwUmAyVJkdqSP6GvzQCuZkE6OBuTvaU5b2obk5rHvDOkWXxAb7xjbVTzIBIg8O5VVhJ-VB8VEbTVsSq9bc0ljkbcyBEzm4JdQRwaCgYKAcgSAQ8SFQHGX2MiR1Owf2aiiEBUsU-LDUDgWg0206",
|
|
||||||
"expires_in": 3599,
|
|
||||||
"refresh_token": "1//03sy-LDVmCMcFCgYIARAAGAMSNwF-L9IrHwwdBBZvg3Hwun9hxHUx_AsJaa1qeozmtvGtdOXKIRIyKjTp7S_b0igvzM_1A5bLH34",
|
|
||||||
"scope": "https://www.googleapis.com/auth/youtube.upload",
|
|
||||||
"token_type": "Bearer"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user