feat: test publish to youtube

This commit is contained in:
2025-11-04 10:20:37 +01:00
parent e0babe90c0
commit a2e0819524
8 changed files with 365 additions and 1 deletions
@@ -40,6 +40,12 @@ const STAGE_CARD_CONTENT = [
description: "Come back when your audio is ready!",
image: ai.rightIcon,
},
{
key: "publisher",
title: "Bena",
description: "Ta vidéo est prête ? Direction YouTube !",
image: ai.bena,
},
];
const WEB_SCROLL_INACTIVE_DELTA = 0.05;
@@ -39,6 +39,12 @@ const STAGE_CARD_CONTENT = [
description: "Come back when your audio is ready!",
image: ai.rightIcon,
},
{
key: "publisher",
title: "Bena",
description: "Ta vidéo est prête ? Direction YouTube !",
image: ai.bena,
},
];
const WEB_SCROLL_INACTIVE_DELTA = 0.05;
+5
View File
@@ -20,6 +20,7 @@ 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 DownloadPrices from "../screens/Production/DownloadPrices";
import DownloadSongs from "../screens/Production/DownloadSongs";
@@ -228,6 +229,10 @@ const baseScreens = [
name: Routes.VideoFinalize,
component: VideoFinalize,
},
{
name: Routes.PublishYoutube,
component: PublishYoutube,
},
{
name: Routes.AllMyPlaylist,
component: AllMyPlaylist,
+1
View File
@@ -58,6 +58,7 @@ export const Routes = {
ChooseDecor: "ChooseDecor",
CreatingDecor: "CreatingDecor",
VideoFinalize: "VideoFinalize",
PublishYoutube: "PublishYoutube",
Create: "Create",
HitParade: "HitParade",
+8
View File
@@ -36,6 +36,14 @@ const CREATE_DATA = [
desc: "Come back when your\naudio is ready!",
type: "Director",
},
{
stageKey: "publisher",
img: ai.bena,
bg: background.productionBG2,
label: "Bena",
desc: "Publions ta vidéo sur YouTube !",
type: "Producteur",
},
];
const NewMusicOptions = () => {
+261
View File
@@ -0,0 +1,261 @@
import { useRoute } from "@react-navigation/native";
import { Image } from "expo-image";
import React, { useCallback, useMemo, useState } from "react";
import { Linking, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { ai, background } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import firebase, { projectsRef, serverTimestamp } from "../../config/firebase";
import Page from "../../layouts/Page";
import { goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
const PublishYoutube = () => {
const route = useRoute();
const routeProjectId = route?.params?.projectId ?? null;
const { userProjects = [], selectedProject } = 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 [saving, setSaving] = useState(false);
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;
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 John pour débloquer la publication YouTube.";
}
if (isPublishing) {
return "Bena s'occupe de mettre ta vidéo en ligne... 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 "Bena taccompagne pour partager 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 {
const publishCallable = firebase
.functions()
.httpsCallable("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 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]);
return (
<Page backgroundImg={background.playbackBG2} 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 }}>
<Image
source={ai.bena}
style={{ width: 220, height: 320 }}
contentFit="contain"
/>
<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 }}>
<GradientButton
title={publishButtonLabel}
onPress={handleMarkPublished}
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}
/>
)}
</View>
</View>
</Page>
);
};
export default PublishYoutube;
+54 -1
View File
@@ -1,6 +1,11 @@
import { Routes } from "../navigation/Routes";
export const CREATION_STAGE_KEYS = ["songwriter", "beatmaker", "director"];
export const CREATION_STAGE_KEYS = [
"songwriter",
"beatmaker",
"director",
"publisher",
];
const getLyricsCount = (project) => {
if (!project) return 0;
@@ -16,6 +21,16 @@ 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)
: [];
@@ -38,9 +53,13 @@ const getStageMetadata = (project) => {
musicStatus,
coverStatus,
playbackStatus,
youtubeStatus,
youtubeError,
isMusicGenerating: musicStatus === "GENERATING",
isCoverGenerating: coverStatus === "GENERATING",
isPlaybackGenerating,
hasYoutubePublication,
isYoutubePublishing,
};
};
@@ -55,6 +74,8 @@ const getStageLockState = (key, metadata) => {
return metadata.lyricsCount <= 0;
case "director":
return !metadata.hasCover;
case "publisher":
return !metadata.hasPlaybackAsset;
default:
return true;
}
@@ -111,6 +132,20 @@ 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 "";
}
@@ -133,6 +168,13 @@ const getStageLockedDescription = (key, metadata) => {
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;
}
@@ -146,6 +188,8 @@ const getStageCompletionState = (key, metadata) => {
return metadata.hasSongUrl || metadata.musicStatus === "GENERATED";
case "director":
return metadata.hasPlaybackAsset;
case "publisher":
return metadata.hasYoutubePublication;
default:
return false;
}
@@ -224,6 +268,15 @@ 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 {};
}
+24
View File
@@ -0,0 +1,24 @@
https://accounts.google.com/o/oauth2/v2/auth
?client_id=305598753437-21ivo6din58aud9dph9bcob06fb52r5n.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
http://localhost:8081/?code=4/0Ab32j90LMDeJDShWSk1OkXwgbjrHS-qQpyYTlXhH9q3nlV0EJa6cTL-7nI9yOKHoz6rgRA&scope=https://www.googleapis.com/auth/youtube.upload
curl -X POST https://oauth2.googleapis.com/token \
-d client_id=305598753437-21ivo6din58aud9dph9bcob06fb52r5n.apps.googleusercontent.com \
-d client_secret=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 \
-d code=4/0Ab32j90LMDeJDShWSk1OkXwgbjrHS-qQpyYTlXhH9q3nlV0EJa6cTL-7nI9yOKHoz6rgRA \
-d grant_type=authorization_code \
-d redirect_uri=http://localhost:8081
{
"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"
}