From ad8042adaac5cf57026eb29f458c3a612eb4e256 Mon Sep 17 00:00:00 2001 From: 240424 <240424@epvc.pt> Date: Thu, 28 May 2026 23:55:35 +0100 Subject: [PATCH] Save current app updates --- src/screens/auth/LoginScreen.tsx | 10 +- src/screens/trip/NewTripScreen.tsx | 731 +++++++++++++++++++++-------- 2 files changed, 535 insertions(+), 206 deletions(-) diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx index 97681aa..e784137 100644 --- a/src/screens/auth/LoginScreen.tsx +++ b/src/screens/auth/LoginScreen.tsx @@ -408,9 +408,7 @@ const styles = StyleSheet.create({ flex: 1, }, scrollContent: { - flexGrow: 1, - justifyContent: 'space-between', - paddingTop: 60, + paddingTop: 48, }, headerContainer: { alignItems: 'center', @@ -460,12 +458,8 @@ const styles = StyleSheet.create({ width: '100%', borderTopLeftRadius: 32, borderTopRightRadius: 32, - borderBottomLeftRadius: 0, - borderBottomRightRadius: 0, padding: 24, - paddingBottom: Platform.OS === 'ios' ? 40 : 24, - flexGrow: 1, - justifyContent: 'flex-start', + paddingBottom: Platform.OS === 'ios' ? 40 : 32, shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.1, diff --git a/src/screens/trip/NewTripScreen.tsx b/src/screens/trip/NewTripScreen.tsx index 749d1c1..03ce195 100644 --- a/src/screens/trip/NewTripScreen.tsx +++ b/src/screens/trip/NewTripScreen.tsx @@ -6,6 +6,85 @@ import { colors } from '../../utils/colors'; import { supabase } from '../../services/supabase'; import { getSpotifyAccessToken, refreshSpotifyToken, clearSpotifyTokens } from '../../auth/spotifyToken'; import { OLLAMA_API_URL } from '../../services/ollama'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +// ── Trip Stop types & route helpers ────────────────────────────────────────── + +interface TripStop { + name: string; + category: string; + address: string; + latitude: number; + longitude: number; + place_id: string; + index: number; +} +type SpotifyArtist = { + id?: string | null; + name?: string | null; +}; + +type SpotifySearchTrack = { + id?: string | null; + uri?: string | null; + duration_ms?: number | null; + is_local?: boolean | null; + is_playable?: boolean | null; + artists?: SpotifyArtist[] | null; +}; + +type SelectedSpotifyTrack = { + id: string; + uri: string; + duration_ms: number; +}; + +/** Maps trip duration (seconds) to the target number of stops. */ +function getStopCount(durationSeconds: number): number { + const minutes = durationSeconds / 60; + if (minutes < 60) return 0; + if (minutes < 180) return 1; + if (minutes < 360) return 3; + if (minutes < 720) return 5; + if (minutes < 1200) return 7; + return 8; +} + +/** + * Returns N {lat, lng} points distributed at equal time intervals along the + * route, using the step list from the Google Directions API leg. + */ +function getRouteWaypoints( + steps: any[], + totalDurationSeconds: number, + count: number +): Array<{ lat: number; lng: number }> { + if (count === 0 || steps.length === 0) return []; + const waypoints: Array<{ lat: number; lng: number }> = []; + let cumulative = 0; + let nextTarget = 1; + for (const step of steps) { + cumulative += (step.duration?.value ?? 0); + while (nextTarget <= count) { + const target = (nextTarget / (count + 1)) * totalDurationSeconds; + if (cumulative >= target) { + waypoints.push({ lat: step.end_location.lat, lng: step.end_location.lng }); + nextTarget++; + } else { + break; + } + } + if (nextTarget > count) break; + } + // Pad with last step coordinates if the route ran short + const last = steps[steps.length - 1]; + while (waypoints.length < count && last) { + waypoints.push({ lat: last.end_location?.lat ?? 0, lng: last.end_location?.lng ?? 0 }); + } + return waypoints; +} + +// ───────────────────────────────────────────────────────────────────────────── // @ts-ignore export default function NewTripScreen({ navigation }) { @@ -40,7 +119,7 @@ export default function NewTripScreen({ navigation }) { const finalDistance = leg.distance.text; const finalDuration = leg.duration.text; const tripDurationMs = leg.duration.value * 1000; - + setDistance(finalDistance); setDuration(finalDuration); @@ -59,7 +138,7 @@ export default function NewTripScreen({ navigation }) { 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 ? "..." : "")); - + if (!res.ok) { throw new Error(`Spotify API returned status ${res.status} for [${label}]: ${rawText.substring(0, 150)}`); } @@ -81,39 +160,39 @@ export default function NewTripScreen({ navigation }) { console.log("SPOTIFY_ACCESS_TOKEN_EXISTS:", !!providerToken); if (providerToken) { - // Validate token via GET /v1/me (free endpoint, no Premium required) - let meRes = await fetch('https://api.spotify.com/v1/me', { - headers: { Authorization: `Bearer ${providerToken}` } - }); - - if (meRes.status === 401) { - console.log("Spotify token is invalid/expired (401), attempting to refresh..."); - const newToken = await refreshSpotifyToken(); - if (newToken) { - providerToken = newToken; - console.log("Spotify token refreshed successfully!"); - } else { - console.log("Failed to refresh Spotify token."); - providerToken = null; - } - } else if (!meRes.ok) { - const meErr = await meRes.text(); - console.warn("Spotify GET /v1/me failed:", meRes.status, meErr); - providerToken = null; - } else { - console.log("Spotify token valid (GET /v1/me returned 200 OK)."); - } + // Validate token via GET /v1/me (free endpoint, no Premium required) + let meRes = await fetch('https://api.spotify.com/v1/me', { + headers: { Authorization: `Bearer ${providerToken}` } + }); + + if (meRes.status === 401) { + console.log("Spotify token is invalid/expired (401), attempting to refresh..."); + const newToken = await refreshSpotifyToken(); + if (newToken) { + providerToken = newToken; + console.log("Spotify token refreshed successfully!"); + } else { + console.log("Failed to refresh Spotify token."); + providerToken = null; + } + } else if (!meRes.ok) { + const meErr = await meRes.text(); + console.warn("Spotify GET /v1/me failed:", meRes.status, meErr); + providerToken = null; + } else { + console.log("Spotify token valid (GET /v1/me returned 200 OK)."); + } } - + if (!providerToken) { - console.log("Spotify token missing or expired, skipping playlist generation."); - playlistCreationFailed = true; - playlistFailureReason = 'token'; - Alert.alert('Spotify Desligado', 'O token do Spotify expirou ou está em falta. Por favor reconecte o Spotify no Perfil.'); + console.log("Spotify token missing or expired, skipping playlist generation."); + playlistCreationFailed = true; + playlistFailureReason = 'token'; + Alert.alert('Spotify Desligado', 'O token do Spotify expirou ou está em falta. Por favor reconecte o Spotify no Perfil.'); } else { // B. Fetch Spotify User ID (reuse /v1/me — already validated above) const spotifyUserRes = await fetch('https://api.spotify.com/v1/me', { - headers: { + headers: { 'Authorization': `Bearer ${providerToken}`, 'Content-Type': 'application/json' } @@ -124,173 +203,408 @@ export default function NewTripScreen({ navigation }) { console.log("SPOTIFY_USER_ID_EXISTS:", !!spotifyUserId); if (!spotifyUserId) throw new Error('Could not fetch Spotify User ID from /v1/me'); - // C. Call Ollama server - const ollamaPrompt = `I am taking a roadtrip from ${origin} to ${destination}. The trip is called "${tripName}" and takes about ${duration}. Reply ONLY with a JSON array of up to 10 Spotify search queries (e.g. genres, moods, or themes) that fit this journey. Example: ["portuguese pop", "italian road trip", "european indie", "summer travel songs"]. No other text.`; - const ollamaRes = await fetch(`${OLLAMA_API_URL}/api/chat`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: "qwen3-coder:30b", - messages: [{ "role": "user", "content": ollamaPrompt }], - stream: false - }) - }); - - let searchQueries: string[] = ["pop hits", "road trip songs", "top hits Portugal", "summer hits", "travel songs"]; // Fallback - 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.startsWith("[")) { - const parsed = JSON.parse(rawAiText); - if (Array.isArray(parsed) && parsed.length > 0) { - const aiQueries = parsed.map(String).slice(0, 10); - searchQueries = [...aiQueries, ...searchQueries]; - } else { - console.log("Ollama returned empty array, using fallbacks"); - } - } else { - console.log("AI returned plain text/error, using fallback queries:", rawAiText); - } - } catch (aiError) { - console.log("AI parsing failed, using fallback queries.", aiError); + // C. Build varied music queries using AI + favorite genre + function shuffleArray(array: T[]): T[] { + const copy = [...array]; + + for (let i = copy.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [copy[i], copy[j]] = [copy[j], copy[i]]; + } + + return copy; } + function cleanSearchQuery(query: string): string { + return query + .replace(/["[\]]/g, "") + .replace(/\s+/g, " ") + .trim(); + } + + function getRandomSpotifyOffset(): number { + const offsets = [0, 10, 20, 30, 40, 50]; + return offsets[Math.floor(Math.random() * offsets.length)]; + } + + function getMainArtistKey(track: any): string { + return ( + track?.artists?.[0]?.id || + track?.artists?.[0]?.name || + "unknown_artist" + ); + } + + async function readFavoriteGenreForPlaylist(): Promise { + try { + const { + data: { session }, + } = await supabase.auth.getSession(); + + const appUserId = session?.user?.id ?? null; + + const possibleKeys = [ + appUserId ? `favoriteGenre:${appUserId}` : "", + appUserId ? `userFavoriteGenre:${appUserId}` : "", + appUserId ? `@roadtripdj:favoriteGenre:${appUserId}` : "", + "favoriteGenre", + "userFavoriteGenre", + "@roadtripdj:favoriteGenre", + ].filter(Boolean); + + for (const key of possibleKeys) { + const value = await AsyncStorage.getItem(key); + + if (value && value.trim().length > 0) { + return value.trim(); + } + } + + if (appUserId) { + try { + const { data } = await supabase + .from("profiles") + .select("favorite_genre") + .eq("id", appUserId) + .maybeSingle(); + + const genre = (data as any)?.favorite_genre; + + if (genre && String(genre).trim().length > 0) { + return String(genre).trim(); + } + } catch { + // Ignore profile lookup errors. Favorite genre is optional. + } + + try { + const { data } = await supabase + .from("profiles") + .select("favoriteGenre") + .eq("id", appUserId) + .maybeSingle(); + + const genre = (data as any)?.favoriteGenre; + + if (genre && String(genre).trim().length > 0) { + return String(genre).trim(); + } + } catch { + // Ignore profile lookup errors. Favorite genre is optional. + } + } + } catch (error) { + console.warn("Failed to read favorite genre:", error); + } + + return ""; + } + + const favoriteGenre = await readFavoriteGenreForPlaylist(); + + console.log("PLAYLIST_FAVORITE_GENRE_USED:", favoriteGenre || "none"); + + const ollamaPrompt = `I am taking a roadtrip from ${origin} to ${destination}. +The trip is called "${tripName}" and takes about ${duration}. +The user's favorite music genre is: "${favoriteGenre || "not set"}". + +Reply ONLY with a JSON array of up to 10 Spotify search queries. +The queries should be varied and specific to this trip. +If a favorite genre is set, strongly include it in the search ideas. +Do NOT return song names. +Do NOT return explanations. +Example: ["rock road trip", "rock hits", "porto travel songs", "portuguese indie"].`; + + let aiQueries: string[] = []; + + try { + const ollamaRes = await fetch(`${OLLAMA_API_URL}/api/chat`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "qwen3-coder:30b", + messages: [{ role: "user", content: ollamaPrompt }], + stream: false, + }), + }); + + const ollamaData = await safeParseJson(ollamaRes, "Ollama"); + let rawAiText = ollamaData?.message?.content || ""; + + rawAiText = rawAiText + .replace(/```json/g, "") + .replace(/```/g, "") + .trim(); + + if (rawAiText.length > 0 && rawAiText.startsWith("[")) { + const parsed = JSON.parse(rawAiText); + + if (Array.isArray(parsed)) { + aiQueries = parsed + .map(String) + .map(cleanSearchQuery) + .filter(Boolean) + .slice(0, 10); + } + } + } catch (aiError) { + console.log("AI parsing failed, using fallback queries.", aiError); + } + + const favoriteGenreQueries = favoriteGenre + ? [ + `${favoriteGenre} road trip`, + `${favoriteGenre} hits`, + `${favoriteGenre} travel songs`, + `${favoriteGenre} driving music`, + `${favoriteGenre} playlist`, + `${favoriteGenre} ${destination}`, + ] + : []; + + const tripSpecificQueries = [ + `${destination} road trip`, + `${origin} to ${destination} music`, + `${tripName} playlist`, + `${destination} travel songs`, + `${origin} ${destination} road trip`, + ]; + + const fallbackQueries = [ + "road trip songs", + "travel songs", + "summer hits", + "top hits Portugal", + "pop hits", + "driving music", + "feel good road trip", + "european travel music", + "party road trip", + "indie road trip", + ]; + + const firstQueries = [ + ...favoriteGenreQueries, + ...aiQueries, + ] + .map(cleanSearchQuery) + .filter(Boolean); + + const remainingQueries = [ + ...tripSpecificQueries, + ...fallbackQueries, + ] + .map(cleanSearchQuery) + .filter(Boolean); + + const searchQueries = Array.from( + new Set([ + ...firstQueries, + ...shuffleArray(remainingQueries), + ]) + ); + + const playlistRandomSeed = `${Date.now()}-${Math.random() + .toString(36) + .slice(2)}`; + + console.log("PLAYLIST_RANDOM_SEED:", playlistRandomSeed); + console.log("PLAYLIST_MUSIC_QUERIES:", searchQueries); + // D. Create empty playlist - const createPlaylistUrl = 'https://api.spotify.com/v1/me/playlists'; + const createPlaylistUrl = "https://api.spotify.com/v1/me/playlists"; const createPlaylistBody = JSON.stringify({ name: tripName, - description: `Roadtrip from ${origin} to ${destination}. Themes: ${searchQueries.join(', ')}`, - public: false + description: `Roadtrip from ${origin} to ${destination}. Themes: ${searchQueries + .slice(0, 8) + .join(", ")}`, + public: false, }); + console.log("CREATE_PLAYLIST_URL:", createPlaylistUrl); console.log("CREATE_PLAYLIST_BODY:", createPlaylistBody); const createPlaylistRes = await fetch(createPlaylistUrl, { - method: 'POST', + method: "POST", headers: { - 'Authorization': `Bearer ${providerToken}`, - 'Content-Type': 'application/json' + Authorization: `Bearer ${providerToken}`, + "Content-Type": "application/json", }, - body: createPlaylistBody + body: createPlaylistBody, }); - + console.log("CREATE_PLAYLIST_HTTP_STATUS:", createPlaylistRes.status); + const createPlaylistResText = await createPlaylistRes.text(); + if (!createPlaylistRes.ok) { - console.log("CREATE_PLAYLIST_RESPONSE_BODY_IF_FAILED:", createPlaylistResText.substring(0, 300)); + console.log( + "CREATE_PLAYLIST_RESPONSE_BODY_IF_FAILED:", + createPlaylistResText.substring(0, 300) + ); + if (createPlaylistRes.status === 403) { - // Stored refresh token predates playlist scopes — clear tokens so next login forces full re-auth await clearSpotifyTokens(); - console.warn("CREATE_PLAYLIST_403: Cleared stale Spotify tokens. User must reconnect."); - Alert.alert( - 'Permissão Spotify Necessária', - 'Reconnect Spotify to grant playlist permissions. Go to Profile and log in with Spotify again.' + + console.warn( + "CREATE_PLAYLIST_403: Cleared stale Spotify tokens. User must reconnect." ); + + Alert.alert( + "Permissão Spotify Necessária", + "Reconecta o Spotify para dar permissão de criar playlists." + ); + playlistCreationFailed = true; - playlistFailureReason = 'scope'; - // Skip further playlist work — trip will still be saved - throw new Error(`CREATE_PLAYLIST_SCOPE_ERROR: Tokens cleared, re-auth required.`); + playlistFailureReason = "scope"; + + throw new Error( + "CREATE_PLAYLIST_SCOPE_ERROR: Tokens cleared, re-auth required." + ); } - throw new Error(`Spotify API returned status ${createPlaylistRes.status} for CreatePlaylist: ${createPlaylistResText.substring(0, 150)}`); + + throw new Error( + `Spotify API returned status ${createPlaylistRes.status + } for CreatePlaylist: ${createPlaylistResText.substring(0, 150)}` + ); } let playlistData: any; + try { playlistData = JSON.parse(createPlaylistResText); - } catch (e) { - throw new Error(`Failed to parse JSON response for CreatePlaylist: ${createPlaylistResText.substring(0, 150)}`); + } catch { + throw new Error( + `Failed to parse JSON response for CreatePlaylist: ${createPlaylistResText.substring( + 0, + 150 + )}` + ); + } + + if (!playlistData.id) { + throw new Error("Could not create playlist"); } - if (!playlistData.id) throw new Error('Could not create playlist'); const playlistId = playlistData.id; generatedPlaylistUrl = playlistData.external_urls.spotify; - // E. Fill playlist with tracks based on duration + // E. Fill playlist with varied tracks based on duration console.log("TARGET_PLAYLIST_DURATION_MS:", tripDurationMs); + let accumulatedDurationMs = 0; - let selectedTracks: { id: string; uri: string; duration_ms: number }[] = []; + const selectedTracks: SelectedSpotifyTrack[] = []; + const selectedTrackIds = new Set(); + const artistCount = new Map(); + let searchRequestsCount = 0; + let queryIndex = 0; + const MAX_SEARCH_REQUESTS = 40; const MAX_TRACKS = 400; - let queryIndex = 0; - let offset = 0; - let noMoreTracks = false; - + const MAX_TRACKS_PER_ARTIST = 3; + while ( - accumulatedDurationMs < tripDurationMs && + accumulatedDurationMs < tripDurationMs && searchRequestsCount < MAX_SEARCH_REQUESTS && selectedTracks.length < MAX_TRACKS && - !noMoreTracks + searchQueries.length > 0 ) { const currentQuery = searchQueries[queryIndex % searchQueries.length]; const queryEncoded = encodeURIComponent(currentQuery); - const searchUrl = `https://api.spotify.com/v1/search?type=track&q=${queryEncoded}&limit=10&market=${spotifyUserCountry}&offset=${offset}`; - - console.log("TRACK_SEARCH_QUERY:", currentQuery, "offset:", offset); + const offset = getRandomSpotifyOffset(); + + const searchUrl = + `https://api.spotify.com/v1/search` + + `?type=track` + + `&q=${queryEncoded}` + + `&limit=10` + + `&market=${spotifyUserCountry}` + + `&offset=${offset}`; + + console.log("TRACK_SEARCH_QUERY:", currentQuery); + console.log("TRACK_SEARCH_OFFSET:", offset); console.log("TRACK_SEARCH_URL:", searchUrl); - + const searchRes = await fetch(searchUrl, { - headers: { - 'Authorization': `Bearer ${providerToken}`, - 'Content-Type': 'application/json' - } + headers: { + Authorization: `Bearer ${providerToken}`, + "Content-Type": "application/json", + }, }); - + console.log("TRACK_SEARCH_STATUS:", searchRes.status); - + if (!searchRes.ok) { - const errText = await searchRes.text(); - console.warn("Spotify search failed:", searchRes.status, errText.substring(0, 150)); - console.log("TRACK_SEARCH_RESPONSE_BODY_IF_FAILED:", errText.substring(0, 300)); - queryIndex++; - offset = 0; - searchRequestsCount++; - continue; - } - - const searchData = await safeParseJson(searchRes, 'SearchTracks'); - const tracks = searchData?.tracks?.items || []; - console.log("TRACKS_RAW_FOUND_COUNT:", tracks.length); - - if (tracks.length === 0) { - queryIndex++; - offset = 0; - if (queryIndex >= searchQueries.length * 3) { - noMoreTracks = true; - } - searchRequestsCount++; - continue; + const errText = await searchRes.text(); + + console.warn( + "Spotify search failed:", + searchRes.status, + errText.substring(0, 150) + ); + + console.log( + "TRACK_SEARCH_RESPONSE_BODY_IF_FAILED:", + errText.substring(0, 300) + ); + + searchRequestsCount++; + queryIndex++; + continue; } + const searchData = (await safeParseJson(searchRes, "SearchTracks")) as any; + + const rawTracks: SpotifySearchTrack[] = Array.isArray(searchData?.tracks?.items) + ? (searchData.tracks.items as SpotifySearchTrack[]) + : []; + + console.log("TRACKS_RAW_FOUND_COUNT:", rawTracks.length); + + const shuffledTracks = shuffleArray(rawTracks); let tracksAfterFilter = 0; - for (const track of tracks) { + for (const track of shuffledTracks) { if (selectedTracks.length >= MAX_TRACKS) break; if (accumulatedDurationMs >= tripDurationMs) break; - - if (track.id && track.uri && track.duration_ms && track.is_local !== true) { - if (track.is_playable === undefined || track.is_playable !== false) { - if (!selectedTracks.some(t => t.id === track.id)) { - selectedTracks.push({ id: track.id, uri: track.uri, duration_ms: track.duration_ms }); - accumulatedDurationMs += track.duration_ms; - tracksAfterFilter++; - } - } - } + + const trackId = track.id; + const trackUri = track.uri; + const trackDurationMs = track.duration_ms; + + if (!trackId) continue; + if (!trackUri) continue; + if (!trackDurationMs) continue; + if (track.is_local === true) continue; + if (track.is_playable === false) continue; + if (selectedTrackIds.has(trackId)) continue; + + const artistKey = getMainArtistKey(track); + const currentArtistCount = artistCount.get(artistKey) ?? 0; + + if (currentArtistCount >= MAX_TRACKS_PER_ARTIST) continue; + + selectedTracks.push({ + id: trackId, + uri: trackUri, + duration_ms: trackDurationMs, + }); + + selectedTrackIds.add(trackId); + artistCount.set(artistKey, currentArtistCount + 1); + + accumulatedDurationMs += trackDurationMs; + tracksAfterFilter++; } - + console.log("TRACKS_AFTER_FILTER_COUNT:", tracksAfterFilter); - - offset += 10; - if (offset >= 1000) { - queryIndex++; - offset = 0; - } + console.log("TRACKS_SELECTED_COUNT:", selectedTracks.length); + console.log("UNIQUE_ARTISTS_COUNT:", artistCount.size); + console.log("SELECTED_TRACKS_TOTAL_DURATION_MS:", accumulatedDurationMs); + searchRequestsCount++; + queryIndex++; } console.log("TRACKS_SELECTED_COUNT:", selectedTracks.length); @@ -298,48 +612,69 @@ export default function NewTripScreen({ navigation }) { if (selectedTracks.length > 0) { // F. Add tracks to playlist in chunks - const trackUris = selectedTracks.map(t => t.uri); + const trackUris = selectedTracks.map((track) => track.uri); const chunkSize = 100; let tracksAddedSuccessfully = true; + for (let i = 0; i < trackUris.length; i += chunkSize) { - const chunk = trackUris.slice(i, i + chunkSize); - - const addTracksRes = await fetch(`https://api.spotify.com/v1/playlists/${playlistId}/items`, { - method: 'POST', + const chunk = trackUris.slice(i, i + chunkSize); + + const addTracksRes = await fetch( + `https://api.spotify.com/v1/playlists/${playlistId}/items`, + { + method: "POST", headers: { - 'Authorization': `Bearer ${providerToken}`, - 'Content-Type': 'application/json' + Authorization: `Bearer ${providerToken}`, + "Content-Type": "application/json", }, - body: JSON.stringify({ uris: chunk }) - }); - - console.log("ADD_TRACKS_HTTP_STATUS:", addTracksRes.status); - if (!addTracksRes.ok) { - const addTracksErr = await addTracksRes.text(); - console.log("ADD_TRACKS_RESPONSE_BODY_IF_FAILED:", addTracksErr.substring(0, 300)); - tracksAddedSuccessfully = false; - throw new Error(`Spotify API returned status ${addTracksRes.status} while adding tracks: ${addTracksErr.substring(0, 150)}`); + body: JSON.stringify({ uris: chunk }), } + ); + + console.log("ADD_TRACKS_HTTP_STATUS:", addTracksRes.status); + + if (!addTracksRes.ok) { + const addTracksErr = await addTracksRes.text(); + + console.log( + "ADD_TRACKS_RESPONSE_BODY_IF_FAILED:", + addTracksErr.substring(0, 300) + ); + + tracksAddedSuccessfully = false; + + throw new Error( + `Spotify API returned status ${addTracksRes.status + } while adding tracks: ${addTracksErr.substring(0, 150)}` + ); + } } - + if (tracksAddedSuccessfully) { - console.log("PLAYLIST_CREATE_SUCCESS:", generatedPlaylistUrl); - playlistCreationFailed = false; - - if (accumulatedDurationMs < tripDurationMs - 60000 && (selectedTracks.length >= MAX_TRACKS || searchRequestsCount >= MAX_SEARCH_REQUESTS || noMoreTracks)) { - const hours = Math.round((accumulatedDurationMs / 3600000) * 10) / 10; - if (hours >= 1) { - playlistSuccessMessage = `Viagem guardada! Playlist criada com ${hours} horas de música.`; - } else { - const minutes = Math.round(accumulatedDurationMs / 60000); - playlistSuccessMessage = `Viagem guardada! Playlist criada com ${minutes} minutos de música.`; - } - } + console.log("PLAYLIST_CREATE_SUCCESS:", generatedPlaylistUrl); + + playlistCreationFailed = false; + + if ( + accumulatedDurationMs < tripDurationMs - 60000 && + (selectedTracks.length >= MAX_TRACKS || + searchRequestsCount >= MAX_SEARCH_REQUESTS) + ) { + const hours = Math.round((accumulatedDurationMs / 3600000) * 10) / 10; + + if (hours >= 1) { + playlistSuccessMessage = `Viagem guardada! Playlist criada com ${hours} horas de música.`; + } else { + const minutes = Math.round(accumulatedDurationMs / 60000); + playlistSuccessMessage = `Viagem guardada! Playlist criada com ${minutes} minutos de música.`; + } + } } } else { - console.warn("No tracks found for queries:", searchQueries); - playlistCreationFailed = true; - playlistFailureReason = 'notracks'; + console.warn("No tracks found for queries:", searchQueries); + + playlistCreationFailed = true; + playlistFailureReason = "notracks"; } } } catch (playlistError: any) { @@ -354,36 +689,36 @@ export default function NewTripScreen({ navigation }) { try { const { data: { session } } = await supabase.auth.getSession(); const userId = session?.user?.id || null; - - const { error: dbError } = await supabase.from('trips').insert({ - user_id: userId, - title: tripName, - origin, - destination, - distance: finalDistance, - duration: finalDuration, - playlist_url: generatedPlaylistUrl + + const { error: dbError } = await supabase.from('trips').insert({ + user_id: userId, + title: tripName, + origin, + destination, + distance: finalDistance, + duration: finalDuration, + playlist_url: generatedPlaylistUrl }); - + if (dbError) { - console.error("DB Insert error:", dbError); - Alert.alert('Erro ao Guardar', 'Não foi possível guardar a viagem na base de dados: ' + dbError.message); + console.error("DB Insert error:", dbError); + Alert.alert('Erro ao Guardar', 'Não foi possível guardar a viagem na base de dados: ' + dbError.message); } else { - if (playlistCreationFailed) { - if (playlistFailureReason === 'notracks') { - Alert.alert('Sucesso!', 'Playlist criada, mas não foram encontradas músicas para adicionar.'); - } else { - Alert.alert('Sucesso!', 'Viagem guardada! A playlist do Spotify não pôde ser criada, mas a viagem foi guardada.'); - } - } else if (generatedPlaylistUrl) { - Alert.alert('Sucesso!', playlistSuccessMessage); - } else { - Alert.alert('Sucesso!', 'Viagem calculada e guardada!'); - } - navigation.goBack(); + if (playlistCreationFailed) { + if (playlistFailureReason === 'notracks') { + Alert.alert('Sucesso!', 'Playlist criada, mas não foram encontradas músicas para adicionar.'); + } else { + Alert.alert('Sucesso!', 'Viagem guardada! A playlist do Spotify não pôde ser criada, mas a viagem foi guardada.'); + } + } else if (generatedPlaylistUrl) { + Alert.alert('Sucesso!', playlistSuccessMessage); + } else { + Alert.alert('Sucesso!', 'Viagem calculada e guardada!'); + } + navigation.goBack(); } } catch (dbEx) { - console.error("Exception during DB save:", dbEx); + console.error("Exception during DB save:", dbEx); } } else {