feat: test publish to youtube
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -58,6 +58,7 @@ export const Routes = {
|
||||
ChooseDecor: "ChooseDecor",
|
||||
CreatingDecor: "CreatingDecor",
|
||||
VideoFinalize: "VideoFinalize",
|
||||
PublishYoutube: "PublishYoutube",
|
||||
|
||||
Create: "Create",
|
||||
HitParade: "HitParade",
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 t’accompagne 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;
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user