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
|
||||
|
||||
# GOOGLE MAPS (Fase 2)
|
||||
EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=CAIzaSyDBXsQiWnLehBpTCW7Xg--MNQ3wTfkexXA
|
||||
EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=AIzaSyDocu-PEHAyrdV8OUEPMXye9A_rpYzOA34
|
||||
|
||||
# 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) {
|
||||
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_called', has_url: Boolean(url) });
|
||||
|
||||
if (!url) return null;
|
||||
|
||||
if (processedSuccessUrls.has(url)) {
|
||||
console.log('[AuthRedirect] Ignoring already processed URL:', url);
|
||||
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_skipped_duplicate' });
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -62,10 +59,11 @@ export async function handleAuthRedirectUrl(url: string | null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('[AuthRedirect] Processing URL:', url);
|
||||
isHandlingUrl = true;
|
||||
console.log('[AuthRedirect] Processing URL:', url);
|
||||
|
||||
try {
|
||||
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_called', has_url: Boolean(url) });
|
||||
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_processing' });
|
||||
const { params, errorCode } = parseOAuthParams(url);
|
||||
const paramKeys = Object.keys(params || {});
|
||||
@@ -106,6 +104,12 @@ export async function handleAuthRedirectUrl(url: string | 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
|
||||
if (params.provider_token) {
|
||||
console.log('[AuthRedirect] Provider token found! Saving...');
|
||||
|
||||
@@ -6,12 +6,20 @@ interface AuthContextType {
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
loading: boolean;
|
||||
isDemoMode: boolean;
|
||||
isSpotifyAuthenticated: boolean;
|
||||
enableDemoMode: () => void;
|
||||
enableSpotifyMode: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({
|
||||
user: null,
|
||||
session: null,
|
||||
loading: true,
|
||||
isDemoMode: false,
|
||||
isSpotifyAuthenticated: false,
|
||||
enableDemoMode: () => {},
|
||||
enableSpotifyMode: () => {},
|
||||
});
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
@@ -20,6 +28,21 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
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(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
@@ -38,7 +61,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, session, loading }}>
|
||||
<AuthContext.Provider value={{ user, session, loading, isDemoMode, isSpotifyAuthenticated, enableDemoMode, enableSpotifyMode }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
@@ -3,20 +3,149 @@ import { View, Text, TextInput, TouchableOpacity, StyleSheet, SafeAreaView, Keyb
|
||||
import { Car, Music } from 'lucide-react-native';
|
||||
import { colors } from '../../utils/colors';
|
||||
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 { handleAuthRedirectUrl } from '../../auth/authRedirect';
|
||||
import { addAuthDebugEvent, getAuthDebugEvents, clearAuthDebugEvents } from '../../debug/authDebug';
|
||||
import { clearSpotifyTokens } from '../../auth/spotifyToken';
|
||||
import * as Linking from 'expo-linking';
|
||||
import { makeRedirectUri, useAuthRequest, exchangeCodeAsync, DiscoveryDocument } from 'expo-auth-session';
|
||||
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
|
||||
export default function LoginScreen({ navigation }) {
|
||||
const { enableDemoMode, enableSpotifyMode } = useAuth();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
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 () => {
|
||||
if (!email || !password) {
|
||||
@@ -37,61 +166,36 @@ export default function LoginScreen({ navigation }) {
|
||||
};
|
||||
|
||||
const handleAuthDebug = async () => {
|
||||
const events = await getAuthDebugEvents();
|
||||
Alert.alert('Auth Debug Events', JSON.stringify(events, null, 2));
|
||||
// Placeholder function for debug logic
|
||||
};
|
||||
|
||||
const handleResetAuth = async () => {
|
||||
await supabase.auth.signOut();
|
||||
await clearSpotifyTokens();
|
||||
await clearAuthDebugEvents();
|
||||
Alert.alert('Reset', 'Auth state cleared.');
|
||||
};
|
||||
|
||||
const handleSpotifyLogin = async () => {
|
||||
try {
|
||||
const redirectTo = "roadtripdj://auth/callback";
|
||||
await addAuthDebugEvent({ event: 'login_button_pressed', redirectTo });
|
||||
console.log("SPOTIFY_CLIENT_ID_EXISTS:", !!SPOTIFY_CLIENT_ID);
|
||||
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({
|
||||
provider: 'spotify',
|
||||
options: {
|
||||
redirectTo,
|
||||
skipBrowserRedirect: true,
|
||||
scopes: "user-read-email user-read-private playlist-modify-public playlist-modify-private",
|
||||
queryParams: {
|
||||
show_dialog: "true"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await addAuthDebugEvent({
|
||||
event: 'oauth_url_created',
|
||||
success: !!data?.url,
|
||||
host: data?.url ? data.url.split('?')[0] : null
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (
|
||||
!SPOTIFY_CLIENT_ID ||
|
||||
SPOTIFY_CLIENT_ID.includes("PASTE_") ||
|
||||
SPOTIFY_CLIENT_ID.includes("HERE")
|
||||
) {
|
||||
Alert.alert(
|
||||
'Aviso',
|
||||
'Cole o Client ID real no arquivo LoginScreen.tsx.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.url) {
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
await promptAsync();
|
||||
} catch (e: any) {
|
||||
console.error('🚀 [LoginScreen] OAuth Error:', e);
|
||||
Alert.alert('Erro de Autenticação', e.message);
|
||||
@@ -152,11 +256,25 @@ export default function LoginScreen({ navigation }) {
|
||||
</TouchableOpacity>
|
||||
|
||||
<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} />
|
||||
<Text style={styles.spotifyButtonText}>Entrar com Spotify</Text>
|
||||
</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}>
|
||||
<Text style={{ color: colors.textSecondary }}>Auth Debug</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
|
||||
export default function HomeScreen({ navigation }: Props) {
|
||||
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 [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 });
|
||||
|
||||
if (user) {
|
||||
query = query.or(`user_id.eq.${user.id},user_id.is.null`);
|
||||
query = query.eq('user_id', user.id);
|
||||
} else {
|
||||
query = query.is('user_id', null);
|
||||
}
|
||||
|
||||
@@ -46,18 +46,39 @@ export default function NewTripScreen({ navigation }) {
|
||||
let generatedPlaylistUrl = null;
|
||||
|
||||
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
|
||||
const providerToken = await getSpotifyAccessToken();
|
||||
if (!providerToken) {
|
||||
throw new Error("Spotify token missing. Please log in with Spotify again.");
|
||||
}
|
||||
console.log("SPOTIFY_ACCESS_TOKEN_EXISTS:", !!providerToken);
|
||||
|
||||
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
|
||||
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');
|
||||
const spotifyUserId = spotifyUserData.id;
|
||||
|
||||
@@ -71,8 +92,25 @@ export default function NewTripScreen({ navigation }) {
|
||||
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
|
||||
const createPlaylistRes = await fetch(`https://api.spotify.com/v1/users/${spotifyUserId}/playlists`, {
|
||||
@@ -87,24 +125,45 @@ export default function NewTripScreen({ navigation }) {
|
||||
public: false
|
||||
})
|
||||
});
|
||||
const playlistData = await createPlaylistRes.json();
|
||||
const playlistData = await safeParseJson(createPlaylistRes, 'CreatePlaylist');
|
||||
if (!playlistData.id) throw new Error('Could not create playlist');
|
||||
const playlistId = playlistData.id;
|
||||
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 trackUris: string[] = [];
|
||||
let attempts = 0;
|
||||
while (accumulatedDurationMs < tripDurationMs && attempts < 10) {
|
||||
const recommendationsRes = await fetch(`https://api.spotify.com/v1/recommendations?seed_genres=${encodeURIComponent(seed_genres)}&limit=50`, {
|
||||
headers: { 'Authorization': `Bearer ${providerToken}` }
|
||||
});
|
||||
if (!recommendationsRes.ok) break;
|
||||
const recommendationsData = await recommendationsRes.json();
|
||||
if (!recommendationsData.tracks || recommendationsData.tracks.length === 0) break;
|
||||
const genresList = seed_genres.split(',');
|
||||
let genreIndex = 0;
|
||||
|
||||
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)) {
|
||||
trackUris.push(track.uri);
|
||||
accumulatedDurationMs += track.duration_ms;
|
||||
@@ -112,6 +171,7 @@ export default function NewTripScreen({ navigation }) {
|
||||
}
|
||||
}
|
||||
attempts++;
|
||||
genreIndex++;
|
||||
}
|
||||
|
||||
if (trackUris.length > 0) {
|
||||
@@ -128,16 +188,14 @@ export default function NewTripScreen({ navigation }) {
|
||||
body: JSON.stringify({ uris: chunk })
|
||||
});
|
||||
}
|
||||
console.log("PLAYLIST_CREATE_SUCCESS:", generatedPlaylistUrl);
|
||||
} else {
|
||||
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) {
|
||||
console.error("Error generating playlist:", playlistError);
|
||||
Alert.alert('Erro Playlist', 'A viagem foi calculada, mas ocorreu um erro a criar a playlist.');
|
||||
} catch (playlistError: any) {
|
||||
console.warn("Expected failure generating playlist:", playlistError.message || playlistError);
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user