diff --git a/functions/index.js b/functions/index.js index 8ec895e..1804f6e 100644 --- a/functions/index.js +++ b/functions/index.js @@ -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') diff --git a/functions/package-lock.json b/functions/package-lock.json index 9c807e3..abf94d9 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -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": { diff --git a/functions/package.json b/functions/package.json index ce6f280..074abfa 100644 --- a/functions/package.json +++ b/functions/package.json @@ -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", diff --git a/functions/src/youtube.js b/functions/src/youtube.js deleted file mode 100644 index 22f0d2c..0000000 --- a/functions/src/youtube.js +++ /dev/null @@ -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 }) - } - } -) diff --git a/src/assets/index.js b/src/assets/index.js index 2cc114a..8451bff 100644 --- a/src/assets/index.js +++ b/src/assets/index.js @@ -270,7 +270,6 @@ export const cardsImg = { writing: require('./icons/writing.png'), studio: require('./icons/studio.png'), video: require('./icons/video.png'), - production: require('./icons/production.png'), } export const subBadges = { diff --git a/src/components/FeatureCarousel/FeatureCarousel.js b/src/components/FeatureCarousel/FeatureCarousel.js index 972afe6..32c70a0 100644 --- a/src/components/FeatureCarousel/FeatureCarousel.js +++ b/src/components/FeatureCarousel/FeatureCarousel.js @@ -2,7 +2,7 @@ import { BlurView } from 'expo-blur' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Animated, FlatList, Platform, StyleSheet, View, useWindowDimensions } from 'react-native' import { responsiveHeight } from '../../actions/responsiveSizes' -import { ai, cardsImg } from '../../assets' +import { ai } from '../../assets' import { gutters } from '../../styles' import { getCreationStageStates } from '../../utils/projectStages' import AnimatedPaginationDot from '../AnimatedPaginationDot/AnimatedPaginationDot' @@ -27,12 +27,6 @@ const STAGE_CARD_CONTENT = [ description: "Theo t'accompagne pour créer ton playback.", 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 diff --git a/src/components/FeatureCarousel/FeatureCarousel.web.js b/src/components/FeatureCarousel/FeatureCarousel.web.js index 69978f6..0e56b9b 100644 --- a/src/components/FeatureCarousel/FeatureCarousel.web.js +++ b/src/components/FeatureCarousel/FeatureCarousel.web.js @@ -8,7 +8,7 @@ import { View, useWindowDimensions, } from 'react-native' -import { ai, cardsImg } from '../../assets' +import { ai } from '../../assets' import { getCreationStageStates } from '../../utils/projectStages' import AnimatedPaginationDot from '../AnimatedPaginationDot/AnimatedPaginationDot' import PersonaCard from '../cards/PersonaCard/PersonaCard' @@ -33,12 +33,6 @@ const STAGE_CARD_CONTENT = [ description: "Theo t'accompagne pour créer ton playback.", 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 diff --git a/src/navigation/MainStack.js b/src/navigation/MainStack.js index 55194c1..fe99793 100644 --- a/src/navigation/MainStack.js +++ b/src/navigation/MainStack.js @@ -25,7 +25,6 @@ import PlaybackOnboarding from '../screens/Playback/PlaybackOnboarding' import RecordPlayback from '../screens/Playback/RecordPlayback' import RecordedPlayback from '../screens/Playback/RecordedPlayback' import VideoFinalize from '../screens/Playback/VideoFinalize' -import PublishYoutube from '../screens/Publishing/PublishYoutube' import PrivacyPolicy from '../screens/PrivacyPolicy' import Subscriptions from '../screens/Subscriptions' import DownloadPrices from '../screens/Production/DownloadPrices' @@ -265,10 +264,6 @@ const baseScreens = [ name: Routes.VideoFinalize, component: VideoFinalize, }, - { - name: Routes.PublishYoutube, - component: PublishYoutube, - }, { name: Routes.AllMyPlaylist, component: AllMyPlaylist, diff --git a/src/navigation/Routes.js b/src/navigation/Routes.js index 42ee0dd..42be972 100644 --- a/src/navigation/Routes.js +++ b/src/navigation/Routes.js @@ -65,7 +65,6 @@ export const Routes = { ChooseDecor: 'ChooseDecor', CreatingDecor: 'CreatingDecor', VideoFinalize: 'VideoFinalize', - PublishYoutube: 'PublishYoutube', Create: 'Create', HitParade: 'HitParade', diff --git a/src/providers/PlayerProvider.js b/src/providers/PlayerProvider.js index f432a8c..ee86b8e 100644 --- a/src/providers/PlayerProvider.js +++ b/src/providers/PlayerProvider.js @@ -44,7 +44,6 @@ const HIDDEN_ROUTE_NAMES = new Set([ Routes.ChooseDecor, Routes.CreatingDecor, Routes.VideoFinalize, - Routes.PublishYoutube, Routes.FlowSelection, Routes.ChooseCoverType, Routes.Settings, diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js index 807ac03..e170f7f 100644 --- a/src/screens/Home/Home.js +++ b/src/screens/Home/Home.js @@ -72,16 +72,6 @@ const STAGE_CARD_CONTENT = [ textAlign: 'left', 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']) diff --git a/src/screens/NewMusicOptions.js b/src/screens/NewMusicOptions.js index 8b8c4a4..b1fca83 100644 --- a/src/screens/NewMusicOptions.js +++ b/src/screens/NewMusicOptions.js @@ -2,7 +2,7 @@ import FontAwesome from '@expo/vector-icons/FontAwesome' import { BlurView } from 'expo-blur' import React, { useCallback, useMemo } from 'react' 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 { navigate } from '../navigation/NavigationService' import { useUserData } from '../providers/UserDataProvider' @@ -36,14 +36,6 @@ const CREATE_DATA = [ desc: "Theo t'accompagne pour créer ton playback.", type: 'Director', }, - { - stageKey: 'publisher', - img: cardsImg.production, - bg: background.productionBG2, - label: 'Publication', - desc: 'Publions ta vidéo sur YouTube !', - type: 'Producteur', - }, ] const NewMusicOptions = () => { diff --git a/src/screens/Production/PlaybackDownload.js b/src/screens/Production/PlaybackDownload.js index b54c7b7..6cdb6ca 100644 --- a/src/screens/Production/PlaybackDownload.js +++ b/src/screens/Production/PlaybackDownload.js @@ -407,7 +407,11 @@ const PlaybackDownload = ({ route }) => { { merge: true } ) - navigate(Routes.PublishYoutube, { projectId: projectForDownload.id }) + setTooltip({ + type: 'success', + text: 'Playback publié sur MusicLand !', + }) + navigate(Routes.Home) } catch (err) { setTooltip({ type: 'error', diff --git a/src/screens/Production/StreamSong.js b/src/screens/Production/StreamSong.js index a99faeb..6288b3f 100644 --- a/src/screens/Production/StreamSong.js +++ b/src/screens/Production/StreamSong.js @@ -42,7 +42,7 @@ const StreamSong = () => { if (!hasAcceptedPublication) { setTooltip({ type: 'error', - text: 'Confirme la diffusion sur Musicland et YouTube avant de publier', + text: 'Confirme la diffusion sur MusicLand avant de publier', }) return } @@ -112,7 +112,7 @@ const StreamSong = () => { 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." /> { - 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 ( - - } - containerStyle={isWeb ? styles.videoPage : undefined} - headerType="NONE" - > - - - - - {hasYoutubePublication ? 'Ta vidéo est déjà sur YouTube' : 'Publier sur YouTube'} - - - {statusMessage} - - {youtubeError && ( - - {youtubeError} - - )} - {hasYoutubePublication && youtubeUrl && ( - - {youtubeUrl} - - )} - - - - setHasAcceptedPublication((prevState) => !prevState)} - label="J'accepte la diffusion de mon contenu sur YouTube." - /> - - Cette confirmation est requise avant toute publication sur YouTube. - - - - - {hasYoutubePublication && youtubeUrl && ( - - )} - {hasYoutubePublication && ( - - )} - - - - - ) -} - -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 diff --git a/src/screens/cover/components/ShareAndCreditsModal.js b/src/screens/cover/components/ShareAndCreditsModal.js index 4e2e36b..e112c35 100644 --- a/src/screens/cover/components/ShareAndCreditsModal.js +++ b/src/screens/cover/components/ShareAndCreditsModal.js @@ -437,7 +437,7 @@ const ShareAndCreditsModal = ({ 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" /> { if (typeof section === 'string') { @@ -36,13 +36,6 @@ const getStageMetadata = (project) => { const hasCoverUrl = !!project?.coverUrl const hasPlaybackAsset = !!project?.playbackUrl 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 musicStatus = project?.musicStatus || null const coverStatus = project?.coverStatus || null @@ -65,13 +58,9 @@ const getStageMetadata = (project) => { coverStatus, playbackStatus, isPlaybackDraftReady, - youtubeStatus, - youtubeError, isMusicGenerating: musicStatus === 'GENERATING', isCoverGenerating: coverStatus === 'GENERATING', isPlaybackGenerating, - hasYoutubePublication, - isYoutubePublishing, } } @@ -89,8 +78,6 @@ const getStageLockState = (key, metadata) => { return true } return !metadata.hasCover - case 'publisher': - return !metadata.hasPlaybackAsset default: return true } @@ -151,20 +138,6 @@ const getStageDescription = (key, metadata) => { return 'Modifier le playback généré' } 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: return '' } @@ -181,11 +154,6 @@ const getStageLockedDescription = (key, metadata) => { return metadata.lyricsCount > 0 ? undefined : 'Créez vos paroles pour débloquer le studio' case 'director': 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: return undefined } @@ -199,8 +167,6 @@ const getStageCompletionState = (key, metadata) => { return metadata.hasSongUrl || metadata.musicStatus === 'GENERATED' case 'director': return metadata.hasPlaybackAsset - case 'publisher': - return metadata.hasYoutubePublication default: return false } @@ -268,15 +234,6 @@ export const getStageAction = (key, project) => { route: Routes.Playback, params: project ? { project } : undefined, } - case 'publisher': { - if (!project?.id) { - return { route: Routes.PublishYoutube } - } - return { - route: Routes.PublishYoutube, - params: { projectId: project.id }, - } - } default: return {} } diff --git a/youtube.md b/youtube.md deleted file mode 100644 index 2bc29a0..0000000 --- a/youtube.md +++ /dev/null @@ -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 : -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" -} -```