Fix: Spotify OAuth RLS integration, profile mapping, and robust AI playlist generation bypass
This commit is contained in:
4
.env
4
.env
@@ -2,7 +2,7 @@ EXPO_PUBLIC_SUPABASE_URL=https://qyvnryhskgmvgjajqqru.supabase.co
|
|||||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_fazCCLmO7XjtryY28ePR-A_CS7aU6fF
|
EXPO_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_fazCCLmO7XjtryY28ePR-A_CS7aU6fF
|
||||||
|
|
||||||
# GOOGLE MAPS (Fase 2)
|
# GOOGLE MAPS (Fase 2)
|
||||||
EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=CAIzaSyDBXsQiWnLehBpTCW7Xg--MNQ3wTfkexXA
|
EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=AIzaSyDocu-PEHAyrdV8OUEPMXye9A_rpYzOA34
|
||||||
|
|
||||||
# SPOTIFY (Fase 3)
|
# SPOTIFY (Fase 3)
|
||||||
EXPO_PUBLIC_SPOTIFY_CLIENT_ID=C7fa1e7acbf7e44f18bf28d74f14fb9cb
|
EXPO_PUBLIC_SPOTIFY_CLIENT_ID=7fa1e7acbf7e44f18bf28d74f14fb9cb
|
||||||
@@ -47,13 +47,10 @@ export function parseOAuthParams(url: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function handleAuthRedirectUrl(url: string | null) {
|
export async function handleAuthRedirectUrl(url: string | null) {
|
||||||
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_called', has_url: Boolean(url) });
|
|
||||||
|
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
|
|
||||||
if (processedSuccessUrls.has(url)) {
|
if (processedSuccessUrls.has(url)) {
|
||||||
console.log('[AuthRedirect] Ignoring already processed URL:', url);
|
console.log('[AuthRedirect] Ignoring already processed URL:', url);
|
||||||
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_skipped_duplicate' });
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +59,11 @@ export async function handleAuthRedirectUrl(url: string | null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AuthRedirect] Processing URL:', url);
|
|
||||||
isHandlingUrl = true;
|
isHandlingUrl = true;
|
||||||
|
console.log('[AuthRedirect] Processing URL:', url);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_called', has_url: Boolean(url) });
|
||||||
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_processing' });
|
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_processing' });
|
||||||
const { params, errorCode } = parseOAuthParams(url);
|
const { params, errorCode } = parseOAuthParams(url);
|
||||||
const paramKeys = Object.keys(params || {});
|
const paramKeys = Object.keys(params || {});
|
||||||
@@ -106,6 +104,12 @@ export async function handleAuthRedirectUrl(url: string | null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore direct Spotify OAuth callbacks (handled by useAuthRequest in LoginScreen)
|
||||||
|
if (params.code && params.state) {
|
||||||
|
console.log("[AuthRedirect] Ignoring direct Spotify OAuth callback");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Save Provider Tokens if present
|
// Save Provider Tokens if present
|
||||||
if (params.provider_token) {
|
if (params.provider_token) {
|
||||||
console.log('[AuthRedirect] Provider token found! Saving...');
|
console.log('[AuthRedirect] Provider token found! Saving...');
|
||||||
|
|||||||
@@ -6,12 +6,20 @@ interface AuthContextType {
|
|||||||
user: User | null;
|
user: User | null;
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
isDemoMode: boolean;
|
||||||
|
isSpotifyAuthenticated: boolean;
|
||||||
|
enableDemoMode: () => void;
|
||||||
|
enableSpotifyMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType>({
|
const AuthContext = createContext<AuthContextType>({
|
||||||
user: null,
|
user: null,
|
||||||
session: null,
|
session: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
isDemoMode: false,
|
||||||
|
isSpotifyAuthenticated: false,
|
||||||
|
enableDemoMode: () => {},
|
||||||
|
enableSpotifyMode: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAuth = () => useContext(AuthContext);
|
export const useAuth = () => useContext(AuthContext);
|
||||||
@@ -20,6 +28,21 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isDemoMode, setIsDemoMode] = useState(false);
|
||||||
|
const [isSpotifyAuthenticated, setIsSpotifyAuthenticated] = useState(false);
|
||||||
|
|
||||||
|
const enableDemoMode = () => {
|
||||||
|
setIsDemoMode(true);
|
||||||
|
setIsSpotifyAuthenticated(false);
|
||||||
|
setUser({ id: '00000000-0000-4000-8000-000000000002', email: 'demo@roadtripdj.com' } as User);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableSpotifyMode = () => {
|
||||||
|
setIsDemoMode(false);
|
||||||
|
setIsSpotifyAuthenticated(true);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||||
@@ -38,7 +61,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ user, session, loading }}>
|
<AuthContext.Provider value={{ user, session, loading, isDemoMode, isSpotifyAuthenticated, enableDemoMode, enableSpotifyMode }}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,20 +3,149 @@ import { View, Text, TextInput, TouchableOpacity, StyleSheet, SafeAreaView, Keyb
|
|||||||
import { Car, Music } from 'lucide-react-native';
|
import { Car, Music } from 'lucide-react-native';
|
||||||
import { colors } from '../../utils/colors';
|
import { colors } from '../../utils/colors';
|
||||||
import { supabase } from '../../services/supabase';
|
import { supabase } from '../../services/supabase';
|
||||||
import { makeRedirectUri } from 'expo-auth-session';
|
|
||||||
import * as QueryParams from 'expo-auth-session/build/QueryParams';
|
|
||||||
import * as Linking from 'expo-linking';
|
|
||||||
import * as WebBrowser from 'expo-web-browser';
|
import * as WebBrowser from 'expo-web-browser';
|
||||||
import { handleAuthRedirectUrl } from '../../auth/authRedirect';
|
import * as Linking from 'expo-linking';
|
||||||
import { addAuthDebugEvent, getAuthDebugEvents, clearAuthDebugEvents } from '../../debug/authDebug';
|
import { makeRedirectUri, useAuthRequest, exchangeCodeAsync, DiscoveryDocument } from 'expo-auth-session';
|
||||||
import { clearSpotifyTokens } from '../../auth/spotifyToken';
|
import { clearSpotifyTokens, setSpotifyToken, setSpotifyRefreshToken } from '../../auth/spotifyToken';
|
||||||
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
|
||||||
|
WebBrowser.maybeCompleteAuthSession();
|
||||||
|
|
||||||
|
// Direct Spotify OAuth Endpoints
|
||||||
|
const discovery: DiscoveryDocument = {
|
||||||
|
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
|
||||||
|
tokenEndpoint: 'https://accounts.spotify.com/api/token',
|
||||||
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export default function LoginScreen({ navigation }) {
|
export default function LoginScreen({ navigation }) {
|
||||||
|
const { enableDemoMode, enableSpotifyMode } = useAuth();
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Temporary Emergency Fix: Hardcoded Client ID
|
||||||
|
const SPOTIFY_CLIENT_ID = "7fa1e7acbf7e44f18bf28d74f14fb9cb";
|
||||||
|
|
||||||
|
// Configure Direct Spotify OAuth
|
||||||
|
const redirectUri = "exp://192.168.1.7:8081/--/auth/callback";
|
||||||
|
|
||||||
|
const [request, response, promptAsync] = useAuthRequest(
|
||||||
|
{
|
||||||
|
clientId: SPOTIFY_CLIENT_ID,
|
||||||
|
scopes: ['user-read-email', 'user-read-private', 'playlist-modify-public', 'playlist-modify-private'],
|
||||||
|
usePKCE: true,
|
||||||
|
redirectUri,
|
||||||
|
},
|
||||||
|
discovery
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle Spotify OAuth Response
|
||||||
|
useEffect(() => {
|
||||||
|
if (response) {
|
||||||
|
console.log("SPOTIFY_AUTH_RESULT:", response);
|
||||||
|
if (response.type === 'success') {
|
||||||
|
const { code } = response.params;
|
||||||
|
console.log("SPOTIFY_CODE_EXISTS:", !!code);
|
||||||
|
exchangeCodeForTokens(code);
|
||||||
|
} else if (response.type === 'error') {
|
||||||
|
Alert.alert('Erro de Autenticação', response.error?.message || 'Falha ao logar com o Spotify');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [response]);
|
||||||
|
|
||||||
|
const exchangeCodeForTokens = async (code: string) => {
|
||||||
|
if (!request?.codeVerifier) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const tokenResult = await exchangeCodeAsync(
|
||||||
|
{
|
||||||
|
clientId: SPOTIFY_CLIENT_ID,
|
||||||
|
code,
|
||||||
|
redirectUri,
|
||||||
|
extraParams: {
|
||||||
|
code_verifier: request.codeVerifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
discovery
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("SPOTIFY_TOKEN_RECEIVED:", !!tokenResult.accessToken);
|
||||||
|
|
||||||
|
if (tokenResult.accessToken) {
|
||||||
|
await setSpotifyToken(tokenResult.accessToken);
|
||||||
|
if (tokenResult.refreshToken) {
|
||||||
|
await setSpotifyRefreshToken(tokenResult.refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 & 2. Check for Supabase session or create an anonymous one
|
||||||
|
const { data: sessionData } = await supabase.auth.getSession();
|
||||||
|
let supabaseUserId = sessionData?.session?.user?.id;
|
||||||
|
|
||||||
|
console.log("SUPABASE_SESSION_EXISTS:", !!sessionData?.session);
|
||||||
|
|
||||||
|
if (!sessionData?.session) {
|
||||||
|
const { data, error } = await supabase.auth.signInAnonymously();
|
||||||
|
if (error) {
|
||||||
|
console.error("Error creating anonymous session:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
supabaseUserId = data.user?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("SUPABASE_USER_ID:", supabaseUserId);
|
||||||
|
console.log("TRIP_INSERT_USER_ID:", supabaseUserId);
|
||||||
|
|
||||||
|
// 7 & 8. Fetch Spotify Profile and update Supabase User Metadata
|
||||||
|
try {
|
||||||
|
console.log("SPOTIFY_PROFILE_FETCH_START");
|
||||||
|
const profileRes = await fetch('https://api.spotify.com/v1/me', {
|
||||||
|
headers: { Authorization: `Bearer ${tokenResult.accessToken}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (profileRes.ok) {
|
||||||
|
const profile = await profileRes.json();
|
||||||
|
console.log("SPOTIFY_PROFILE_NAME:", profile.display_name);
|
||||||
|
console.log("SPOTIFY_PROFILE_EMAIL_EXISTS:", !!profile.email);
|
||||||
|
console.log("SPOTIFY_USER_ID:", profile.id);
|
||||||
|
console.log("SPOTIFY_ACCESS_TOKEN_EXISTS:", !!tokenResult.accessToken);
|
||||||
|
|
||||||
|
// Force session refresh to ensure AuthContext picks up the new metadata instantly
|
||||||
|
const { data: updatedUser } = await supabase.auth.updateUser({
|
||||||
|
data: {
|
||||||
|
display_name: profile.display_name,
|
||||||
|
name: profile.display_name,
|
||||||
|
spotify_id: profile.id,
|
||||||
|
email: profile.email,
|
||||||
|
avatar_url: profile.images?.[0]?.url
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Explicitly refresh session if needed
|
||||||
|
if (updatedUser) {
|
||||||
|
await supabase.auth.refreshSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (profileError) {
|
||||||
|
console.error("Error fetching Spotify profile:", profileError);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("LOGIN_MODE:", "spotify");
|
||||||
|
console.log("ENTERING_SPOTIFY_MODE");
|
||||||
|
|
||||||
|
// Trigger app main flow as Spotify authenticated user
|
||||||
|
enableSpotifyMode();
|
||||||
|
} else {
|
||||||
|
Alert.alert('Erro de Autenticação', 'Não foi possível obter o token de acesso do Spotify.');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('🚀 [LoginScreen] Token Exchange Error:', e);
|
||||||
|
Alert.alert('Erro de Autenticação', 'Não foi possível trocar o código pelo token.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
@@ -37,61 +166,36 @@ export default function LoginScreen({ navigation }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAuthDebug = async () => {
|
const handleAuthDebug = async () => {
|
||||||
const events = await getAuthDebugEvents();
|
// Placeholder function for debug logic
|
||||||
Alert.alert('Auth Debug Events', JSON.stringify(events, null, 2));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetAuth = async () => {
|
const handleResetAuth = async () => {
|
||||||
await supabase.auth.signOut();
|
await supabase.auth.signOut();
|
||||||
await clearSpotifyTokens();
|
await clearSpotifyTokens();
|
||||||
await clearAuthDebugEvents();
|
|
||||||
Alert.alert('Reset', 'Auth state cleared.');
|
Alert.alert('Reset', 'Auth state cleared.');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSpotifyLogin = async () => {
|
const handleSpotifyLogin = async () => {
|
||||||
try {
|
try {
|
||||||
const redirectTo = "roadtripdj://auth/callback";
|
console.log("SPOTIFY_CLIENT_ID_EXISTS:", !!SPOTIFY_CLIENT_ID);
|
||||||
await addAuthDebugEvent({ event: 'login_button_pressed', redirectTo });
|
console.log("SPOTIFY_CLIENT_ID_LENGTH:", SPOTIFY_CLIENT_ID.length);
|
||||||
|
console.log("SPOTIFY_CLIENT_ID_FIRST_6:", SPOTIFY_CLIENT_ID.slice(0, 6));
|
||||||
|
console.log("SPOTIFY_REDIRECT_URI:", redirectUri);
|
||||||
|
console.log("FULL_SPOTIFY_AUTH_URL:", request?.url);
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
if (
|
||||||
provider: 'spotify',
|
!SPOTIFY_CLIENT_ID ||
|
||||||
options: {
|
SPOTIFY_CLIENT_ID.includes("PASTE_") ||
|
||||||
redirectTo,
|
SPOTIFY_CLIENT_ID.includes("HERE")
|
||||||
skipBrowserRedirect: true,
|
) {
|
||||||
scopes: "user-read-email user-read-private playlist-modify-public playlist-modify-private",
|
Alert.alert(
|
||||||
queryParams: {
|
'Aviso',
|
||||||
show_dialog: "true"
|
'Cole o Client ID real no arquivo LoginScreen.tsx.'
|
||||||
}
|
);
|
||||||
}
|
return;
|
||||||
});
|
|
||||||
|
|
||||||
await addAuthDebugEvent({
|
|
||||||
event: 'oauth_url_created',
|
|
||||||
success: !!data?.url,
|
|
||||||
host: data?.url ? data.url.split('?')[0] : null
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.url) {
|
await promptAsync();
|
||||||
const result = await WebBrowser.openAuthSessionAsync(data.url, redirectTo);
|
|
||||||
await addAuthDebugEvent({ event: 'web_browser_closed', type: result.type });
|
|
||||||
if (result.type === 'success' && result.url) {
|
|
||||||
await addAuthDebugEvent({ event: 'web_browser_success_url_received', success: true });
|
|
||||||
await addAuthDebugEvent({ event: 'processing_web_browser_success_url' });
|
|
||||||
|
|
||||||
await handleAuthRedirectUrl(result.url);
|
|
||||||
|
|
||||||
const { data: sessionData } = await supabase.auth.getSession();
|
|
||||||
await addAuthDebugEvent({
|
|
||||||
event: 'post_browser_getSession',
|
|
||||||
user_exists: Boolean(sessionData.session?.user),
|
|
||||||
provider_token_exists: Boolean((sessionData.session as any)?.provider_token)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('🚀 [LoginScreen] OAuth Error:', e);
|
console.error('🚀 [LoginScreen] OAuth Error:', e);
|
||||||
Alert.alert('Erro de Autenticação', e.message);
|
Alert.alert('Erro de Autenticação', e.message);
|
||||||
@@ -152,11 +256,25 @@ export default function LoginScreen({ navigation }) {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.spotifyButton} onPress={handleSpotifyLogin}>
|
<TouchableOpacity style={styles.spotifyButton} onPress={handleSpotifyLogin}>
|
||||||
{/* Note: In a real app we would use an actual Spotify SVG logo. Using Music icon for now as a placeholder for the Spotify logo. */}
|
|
||||||
<Music color={colors.white} size={20} style={styles.spotifyIcon} />
|
<Music color={colors.white} size={20} style={styles.spotifyIcon} />
|
||||||
<Text style={styles.spotifyButtonText}>Entrar com Spotify</Text>
|
<Text style={styles.spotifyButtonText}>Entrar com Spotify</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{__DEV__ && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.primaryButton, { backgroundColor: '#333', marginBottom: 24 }]}
|
||||||
|
onPress={() => {
|
||||||
|
console.log("DEMO_BYPASS_PRESSED");
|
||||||
|
console.log("LOGIN_MODE:", "demo");
|
||||||
|
console.log("ENTERING_DEMO_MODE");
|
||||||
|
console.log("AVAILABLE_ROUTE_NAMES", navigation.getState()?.routeNames);
|
||||||
|
enableDemoMode();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.primaryButtonText}>Continuar sem Spotify (demo)</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
<TouchableOpacity style={{ padding: 10, alignItems: 'center' }} onPress={handleAuthDebug}>
|
<TouchableOpacity style={{ padding: 10, alignItems: 'center' }} onPress={handleAuthDebug}>
|
||||||
<Text style={{ color: colors.textSecondary }}>Auth Debug</Text>
|
<Text style={{ color: colors.textSecondary }}>Auth Debug</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface Props {
|
|||||||
|
|
||||||
export default function HomeScreen({ navigation }: Props) {
|
export default function HomeScreen({ navigation }: Props) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const userName = user?.user_metadata?.name || 'Viajante';
|
const userName = user?.user_metadata?.display_name || user?.user_metadata?.name || user?.email || user?.user_metadata?.email || 'Viajante';
|
||||||
const initial = userName.charAt(0).toUpperCase();
|
const initial = userName.charAt(0).toUpperCase();
|
||||||
|
|
||||||
const [trips, setTrips] = useState<any[]>([]);
|
const [trips, setTrips] = useState<any[]>([]);
|
||||||
@@ -26,7 +26,7 @@ export default function HomeScreen({ navigation }: Props) {
|
|||||||
let query = supabase.from('trips').select('*').order('created_at', { ascending: false });
|
let query = supabase.from('trips').select('*').order('created_at', { ascending: false });
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
query = query.or(`user_id.eq.${user.id},user_id.is.null`);
|
query = query.eq('user_id', user.id);
|
||||||
} else {
|
} else {
|
||||||
query = query.is('user_id', null);
|
query = query.is('user_id', null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,18 +46,39 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
let generatedPlaylistUrl = null;
|
let generatedPlaylistUrl = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log("GENERATING_PLAYLIST_FOR_TRIP:", tripName);
|
||||||
|
console.log("PLAYLIST_CREATE_START");
|
||||||
|
|
||||||
|
// Helper for robust parsing
|
||||||
|
const safeParseJson = async (res: Response, label: string) => {
|
||||||
|
const rawText = await res.text();
|
||||||
|
console.log(`PLAYLIST_API_STATUS [${label}]:`, res.status);
|
||||||
|
console.log(`PLAYLIST_API_CONTENT_TYPE [${label}]:`, res.headers.get("content-type"));
|
||||||
|
console.log(`PLAYLIST_API_RAW_RESPONSE [${label}]:`, rawText.substring(0, 300) + (rawText.length > 300 ? "..." : ""));
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(rawText);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Playlist API returned non-JSON response [${label}]: ${rawText}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// A. Get provider token
|
// A. Get provider token
|
||||||
const providerToken = await getSpotifyAccessToken();
|
const providerToken = await getSpotifyAccessToken();
|
||||||
if (!providerToken) {
|
console.log("SPOTIFY_ACCESS_TOKEN_EXISTS:", !!providerToken);
|
||||||
throw new Error("Spotify token missing. Please log in with Spotify again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerToken) {
|
if (!providerToken) {
|
||||||
|
console.log("Spotify token missing, skipping playlist generation.");
|
||||||
|
Alert.alert('Aviso', 'Spotify token missing, please login again');
|
||||||
|
} else {
|
||||||
// B. Fetch Spotify User ID
|
// B. Fetch Spotify User ID
|
||||||
const spotifyUserRes = await fetch('https://api.spotify.com/v1/me', {
|
const spotifyUserRes = await fetch('https://api.spotify.com/v1/me', {
|
||||||
headers: { 'Authorization': `Bearer ${providerToken}` }
|
headers: {
|
||||||
|
'Authorization': `Bearer ${providerToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const spotifyUserData = await spotifyUserRes.json();
|
const spotifyUserData = await safeParseJson(spotifyUserRes, 'SpotifyUser');
|
||||||
if (!spotifyUserData.id) throw new Error('Could not fetch Spotify User ID');
|
if (!spotifyUserData.id) throw new Error('Could not fetch Spotify User ID');
|
||||||
const spotifyUserId = spotifyUserData.id;
|
const spotifyUserId = spotifyUserData.id;
|
||||||
|
|
||||||
@@ -71,8 +92,25 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
stream: false
|
stream: false
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const ollamaData = await ollamaRes.json();
|
|
||||||
const seed_genres = ollamaData.message.content.trim().replace(/\s+/g, '').toLowerCase();
|
let seed_genres = "pop,road-trip,happy"; // Fallback genres
|
||||||
|
try {
|
||||||
|
const ollamaData = await safeParseJson(ollamaRes, 'Ollama');
|
||||||
|
let rawAiText = ollamaData?.message?.content || "";
|
||||||
|
|
||||||
|
// Clean AI text
|
||||||
|
rawAiText = rawAiText.replace(/```json/g, '').replace(/```/g, '').trim();
|
||||||
|
|
||||||
|
if (rawAiText.length > 0 && !rawAiText.toLowerCase().startsWith("a ")) {
|
||||||
|
seed_genres = rawAiText.replace(/\s+/g, '').toLowerCase();
|
||||||
|
// Spotify limits to 5 seed genres, let's keep it clean
|
||||||
|
seed_genres = seed_genres.split(',').slice(0, 5).join(',');
|
||||||
|
} else {
|
||||||
|
console.log("AI returned plain text/error, using fallback genres:", rawAiText);
|
||||||
|
}
|
||||||
|
} catch (aiError) {
|
||||||
|
console.log("AI parsing failed, using fallback genres.", aiError);
|
||||||
|
}
|
||||||
|
|
||||||
// D. Create empty playlist
|
// D. Create empty playlist
|
||||||
const createPlaylistRes = await fetch(`https://api.spotify.com/v1/users/${spotifyUserId}/playlists`, {
|
const createPlaylistRes = await fetch(`https://api.spotify.com/v1/users/${spotifyUserId}/playlists`, {
|
||||||
@@ -87,24 +125,45 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
public: false
|
public: false
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const playlistData = await createPlaylistRes.json();
|
const playlistData = await safeParseJson(createPlaylistRes, 'CreatePlaylist');
|
||||||
if (!playlistData.id) throw new Error('Could not create playlist');
|
if (!playlistData.id) throw new Error('Could not create playlist');
|
||||||
const playlistId = playlistData.id;
|
const playlistId = playlistData.id;
|
||||||
generatedPlaylistUrl = playlistData.external_urls.spotify;
|
generatedPlaylistUrl = playlistData.external_urls.spotify;
|
||||||
|
|
||||||
// E. Fetch Spotify track recommendations
|
// E. Fetch Spotify track recommendations via Search (does not require Premium)
|
||||||
let accumulatedDurationMs = 0;
|
let accumulatedDurationMs = 0;
|
||||||
let trackUris: string[] = [];
|
let trackUris: string[] = [];
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
while (accumulatedDurationMs < tripDurationMs && attempts < 10) {
|
const genresList = seed_genres.split(',');
|
||||||
const recommendationsRes = await fetch(`https://api.spotify.com/v1/recommendations?seed_genres=${encodeURIComponent(seed_genres)}&limit=50`, {
|
let genreIndex = 0;
|
||||||
headers: { 'Authorization': `Bearer ${providerToken}` }
|
|
||||||
});
|
|
||||||
if (!recommendationsRes.ok) break;
|
|
||||||
const recommendationsData = await recommendationsRes.json();
|
|
||||||
if (!recommendationsData.tracks || recommendationsData.tracks.length === 0) break;
|
|
||||||
|
|
||||||
for (const track of recommendationsData.tracks) {
|
while (accumulatedDurationMs < tripDurationMs && attempts < 10) {
|
||||||
|
const currentGenre = genresList[genreIndex % genresList.length] || 'pop';
|
||||||
|
const query = encodeURIComponent(`genre:${currentGenre}`);
|
||||||
|
const searchRes = await fetch(`https://api.spotify.com/v1/search?type=track&q=${query}&limit=50&offset=${attempts * 50}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${providerToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!searchRes.ok) {
|
||||||
|
const errText = await searchRes.text();
|
||||||
|
console.error("Search failed:", errText);
|
||||||
|
Alert.alert('Erro Spotify', `Aviso ao adicionar músicas: ${errText.substring(0, 100)}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchData = await safeParseJson(searchRes, 'SearchTracks');
|
||||||
|
const tracks = searchData?.tracks?.items;
|
||||||
|
|
||||||
|
if (!tracks || tracks.length === 0) {
|
||||||
|
genreIndex++;
|
||||||
|
attempts++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const track of tracks) {
|
||||||
if (!trackUris.includes(track.uri)) {
|
if (!trackUris.includes(track.uri)) {
|
||||||
trackUris.push(track.uri);
|
trackUris.push(track.uri);
|
||||||
accumulatedDurationMs += track.duration_ms;
|
accumulatedDurationMs += track.duration_ms;
|
||||||
@@ -112,6 +171,7 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
attempts++;
|
attempts++;
|
||||||
|
genreIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackUris.length > 0) {
|
if (trackUris.length > 0) {
|
||||||
@@ -128,16 +188,14 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
body: JSON.stringify({ uris: chunk })
|
body: JSON.stringify({ uris: chunk })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
console.log("PLAYLIST_CREATE_SUCCESS:", generatedPlaylistUrl);
|
||||||
} else {
|
} else {
|
||||||
console.error("No tracks found for genres:", seed_genres);
|
console.error("No tracks found for genres:", seed_genres);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.error("Spotify token missing, skipping playlist generation.");
|
|
||||||
Alert.alert('Aviso', 'Sessão Spotify não encontrada. A viagem será guardada sem playlist.');
|
|
||||||
}
|
}
|
||||||
} catch (playlistError) {
|
} catch (playlistError: any) {
|
||||||
console.error("Error generating playlist:", playlistError);
|
console.warn("Expected failure generating playlist:", playlistError.message || playlistError);
|
||||||
Alert.alert('Erro Playlist', 'A viagem foi calculada, mas ocorreu um erro a criar a playlist.');
|
Alert.alert('Erro Playlist', `A viagem foi calculada, mas a playlist falhou: ${playlistError.message?.substring(0, 50) || 'Erro Desconhecido'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// G. Save to Supabase unconditionally if route is valid
|
// G. Save to Supabase unconditionally if route is valid
|
||||||
|
|||||||
Reference in New Issue
Block a user