diff --git a/src/screens/trip/NewTripScreen.tsx b/src/screens/trip/NewTripScreen.tsx index 03ce195..a644c7a 100644 --- a/src/screens/trip/NewTripScreen.tsx +++ b/src/screens/trip/NewTripScreen.tsx @@ -84,6 +84,62 @@ function getRouteWaypoints( return waypoints; } +const artistGenreCache = new Map(); + +async function getArtistGenres(artistId: string, token: string): Promise { + if (!artistId) return []; + if (artistGenreCache.has(artistId)) { + return artistGenreCache.get(artistId) || []; + } + try { + const res = await fetch(`https://api.spotify.com/v1/artists/${artistId}`, { + headers: { Authorization: `Bearer ${token}` } + }); + if (res.ok) { + const data = await res.json(); + const genres = data.genres || []; + artistGenreCache.set(artistId, genres); + return genres; + } else { + console.warn(`[SpotifyPlaylists] Failed to fetch artist ${artistId}: status ${res.status}`); + } + } catch (err) { + console.warn(`[SpotifyPlaylists] Error fetching artist ${artistId}:`, err); + } + return []; +} + +function genreMatches(artistGenres: string[], favoriteGenre: string): boolean { + const normalizedFav = favoriteGenre.toLowerCase().trim(); + + const synonymMap: Record = { + 'fado': ['fado', 'portuguese fado'], + 'rock': ['rock', 'classic rock', 'alternative rock'], + 'rap': ['rap', 'hip hop', 'portuguese hip hop', 'hip-hop'], + 'hip hop': ['hip hop', 'rap', 'hip-hop', 'portuguese hip hop'], + 'hip-hop': ['hip hop', 'rap', 'hip-hop', 'portuguese hip hop'], + 'pop': ['pop', 'portuguese pop'], + 'funk': ['funk', 'baile funk', 'funk carioca'], + 'electronic': ['electronic', 'edm', 'house', 'techno', 'electro'] + }; + + const allowedSynonyms = [ + normalizedFav, + ...(synonymMap[normalizedFav] || []) + ]; + + for (const genre of artistGenres) { + const normalizedGenre = genre.toLowerCase(); + for (const syn of allowedSynonyms) { + if (normalizedGenre === syn || normalizedGenre.includes(syn)) { + return true; + } + } + } + + return false; +} + // ───────────────────────────────────────────────────────────────────────────── // @ts-ignore @@ -127,6 +183,8 @@ export default function NewTripScreen({ navigation }) { let playlistCreationFailed = false; let playlistFailureReason = ''; let playlistSuccessMessage = 'Viagem e playlist criadas com sucesso.'; + let hasGenre = false; + let accumulatedDurationMs = 0; try { console.log("GENERATING_PLAYLIST_FOR_TRIP:", tripName); @@ -304,311 +362,313 @@ export default function NewTripScreen({ navigation }) { 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"}". + hasGenre = Boolean(favoriteGenre && favoriteGenre.trim().length > 0); + const cleanGenre = hasGenre ? favoriteGenre.trim().toLowerCase() : ""; -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"].`; + console.log('[Playlist] favoriteMusicStyle:', favoriteGenre || '(empty)'); - let aiQueries: string[] = []; + // ── Style → curated query map (artist-based; genre: filter unreliable for tracks) + const STYLE_QUERY_MAP: Record = { + fado: [ + 'Amália Rodrigues', 'Mariza fado', 'Carlos do Carmo', 'Ana Moura', + 'Dulce Pontes', 'Camané', 'Carminho fado', 'Mísia fado', + 'Madredeus fado', 'João Braga fado', 'Rodrigo fado', 'Celeste Rodrigues', + 'fado português', 'fado clássico', 'fado moderno', 'fado novo', + 'músicas de fado', 'fado Lisboa', 'fado Coimbra', + ], + rock: [ + 'classic rock hits', 'rock road trip', 'alternative rock', + 'rock classics', 'rock driving', 'hard rock hits', 'rock anthems', + 'indie rock', 'portuguese rock', 'rock nacional', + ], + rap: [ + 'rap português', 'portuguese hip hop', 'rap viagem', 'hip hop road trip', + 'trap hits', 'rap nacional', 'rap clássico', 'rap popular', + ], + 'hip hop': [ + 'hip hop road trip', 'hip hop hits', 'rap viagem', + 'portuguese hip hop', 'trap music', 'hip hop classics', + ], + pop: [ + 'pop português', 'pop hits', 'pop road trip', 'pop viagem', + 'pop clássico', 'pop moderno', 'pop nacional', + ], + funk: [ + 'funk hits', 'baile funk', 'funk road trip', 'funk clássico', + 'funk carioca', 'funk nacional', + ], + electronic: [ + 'electronic road trip', 'edm hits', 'house music driving', + 'techno', 'electronic dance', 'electro hits', + ], + }; + // Resolve style queries: use STYLE_QUERY_MAP if we recognise the genre, else build generic ones + const styleQueries: string[] = hasGenre + ? ( + STYLE_QUERY_MAP[cleanGenre] ?? + [ + cleanGenre, + `${cleanGenre} hits`, + `${cleanGenre} popular`, + `${cleanGenre} viagem`, + `${cleanGenre} clássico`, + `${cleanGenre} moderno`, + `músicas de ${cleanGenre}`, + ] + ) + : []; + + // AI supplement: ask Ollama for artists/songs of the style only when genre is set + const ollamaPrompt = hasGenre + ? `The user's favorite music genre is: "${cleanGenre}". +Reply ONLY with a JSON array of up to 10 well-known song titles or artist names that belong strictly to the "${cleanGenre}" genre. +Do NOT include songs from other genres. +Do NOT include explanation or markdown. +Example for fado: ["Amália Rodrigues", "Mariza", "Ana Moura fado", "Carlos do Carmo"].` + : `I am taking a roadtrip from ${origin} to ${destination} ("${tripName}", ${duration}). +Reply ONLY with a JSON array of up to 10 varied Spotify search queries for a road trip playlist. +Do NOT return song names. Do NOT return explanations. +Example: ["feel good road trip", "summer hits", "driving pop hits"].`; + + let aiArtistQueries: string[] = []; try { const ollamaRes = await fetch(`${OLLAMA_API_URL}/api/chat`, { - method: "POST", - headers: { "Content-Type": "application/json" }, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - model: "qwen3-coder:30b", - messages: [{ role: "user", content: ollamaPrompt }], + 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 ollamaData = await safeParseJson(ollamaRes, 'Ollama'); + let rawAiText = (ollamaData?.message?.content || '') + .replace(/```json/g, '').replace(/```/g, '').trim(); + if (rawAiText.startsWith('[')) { const parsed = JSON.parse(rawAiText); - if (Array.isArray(parsed)) { - aiQueries = parsed - .map(String) - .map(cleanSearchQuery) - .filter(Boolean) - .slice(0, 10); + aiArtistQueries = parsed.map(String).map(cleanSearchQuery).filter(Boolean).slice(0, 10); } } - } catch (aiError) { - console.log("AI parsing failed, using fallback queries.", aiError); + } catch { + console.log('[Playlist] AI query generation failed; continuing without it.'); } - const favoriteGenreQueries = favoriteGenre - ? [ - `${favoriteGenre} road trip`, - `${favoriteGenre} hits`, - `${favoriteGenre} travel songs`, - `${favoriteGenre} driving music`, - `${favoriteGenre} playlist`, - `${favoriteGenre} ${destination}`, - ] - : []; + // Generic roadtrip filler queries (only used in Phase 2 when no genre, or to pad when genre exhausted) + const genericFillerQueries = [ + `${destination} road trip`, `${origin} to ${destination} music`, + 'road trip hits', 'travel songs', 'driving music', + 'summer hits', 'feel good road trip', 'top hits Portugal', + ].map(cleanSearchQuery).filter(Boolean); - const tripSpecificQueries = [ - `${destination} road trip`, - `${origin} to ${destination} music`, - `${tripName} playlist`, - `${destination} travel songs`, - `${origin} ${destination} road trip`, - ]; + // ── Target track count (based on trip duration, ~4 min per song) + const tripDurationMinutes = tripDurationMs / 60000; + const TARGET_TRACK_COUNT = Math.max(10, Math.ceil(tripDurationMinutes / 4)); + const STYLE_RATIO = 0.80; + const styleTargetCount = hasGenre ? Math.ceil(TARGET_TRACK_COUNT * STYLE_RATIO) : 0; - 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", - ]; + console.log('[Playlist] targetTrackCount:', TARGET_TRACK_COUNT); + console.log('[Playlist] styleTargetCount:', styleTargetCount); + console.log('[Playlist] styleQueries:', styleQueries); - const firstQueries = [ - ...favoriteGenreQueries, - ...aiQueries, - ] - .map(cleanSearchQuery) - .filter(Boolean); + const MAX_TRACKS_PER_ARTIST = 3; - const remainingQueries = [ - ...tripSpecificQueries, - ...fallbackQueries, - ] - .map(cleanSearchQuery) - .filter(Boolean); + // Helper to run Spotify track search for a given query and return accepted tracks + const selectedTrackIds = new Set(); + const artistCount = new Map(); + let totalRawResultsCount = 0; + let tracksRejectedCount = 0; + let searchRequestsCount = 0; - const searchQueries = Array.from( - new Set([ - ...firstQueries, - ...shuffleArray(remainingQueries), - ]) - ); + const searchAndFilter = async ( + query: string, + validateGenre: boolean, + ): Promise => { + const results: SelectedSpotifyTrack[] = []; + const offsets = [0, 10, 20, 30]; + for (const offset of offsets) { + if (accumulatedDurationMs >= tripDurationMs) break; + if (searchRequestsCount >= 60) break; - const playlistRandomSeed = `${Date.now()}-${Math.random() - .toString(36) - .slice(2)}`; + const queryEncoded = encodeURIComponent(query); + const searchUrl = + `https://api.spotify.com/v1/search?type=track` + + `&q=${queryEncoded}&limit=20&market=${spotifyUserCountry}&offset=${offset}`; - console.log("PLAYLIST_RANDOM_SEED:", playlistRandomSeed); - console.log("PLAYLIST_MUSIC_QUERIES:", searchQueries); + console.log('TRACK_SEARCH_QUERY:', query, '| offset:', offset); - // D. Create empty playlist - const createPlaylistUrl = "https://api.spotify.com/v1/me/playlists"; + const searchRes = await fetch(searchUrl, { + headers: { Authorization: `Bearer ${providerToken}`, 'Content-Type': 'application/json' }, + }); + + searchRequestsCount++; + + if (!searchRes.ok) { + const errText = await searchRes.text(); + console.warn('[Playlist] Spotify search failed:', searchRes.status, errText.substring(0, 150)); + break; + } + + const searchData = (await safeParseJson(searchRes, 'SearchTracks')) as any; + const rawTracks: SpotifySearchTrack[] = Array.isArray(searchData?.tracks?.items) + ? searchData.tracks.items : []; + + totalRawResultsCount += rawTracks.length; + console.log(`[Playlist] Query "${query}" offset ${offset}: ${rawTracks.length} raw tracks`); + + for (const track of rawTracks) { + if (accumulatedDurationMs >= tripDurationMs) break; + + const trackId = track.id; + const trackUri = track.uri; + const trackDurationMs = track.duration_ms; + + if (!trackId || !trackUri || !trackDurationMs) continue; + if (track.is_local === true) continue; + if (track.is_playable === false) continue; + if (selectedTrackIds.has(trackId)) continue; + + if (validateGenre && hasGenre) { + const mainArtistId = track.artists?.[0]?.id; + if (!mainArtistId) { tracksRejectedCount++; continue; } + const artistGenres = await getArtistGenres(mainArtistId, providerToken); + if (!genreMatches(artistGenres, cleanGenre)) { + console.log(`[Playlist] REJECTED: "${track.artists?.[0]?.name}" genres=[${artistGenres.join(', ')}]`); + tracksRejectedCount++; + continue; + } + } + + const artistKey = getMainArtistKey(track); + if ((artistCount.get(artistKey) ?? 0) >= MAX_TRACKS_PER_ARTIST) continue; + + results.push({ id: trackId, uri: trackUri, duration_ms: trackDurationMs }); + } + + // Don't paginate if we already have plenty of tracks from this query + if (rawTracks.length < 20) break; + } + return results; + }; + + // D. Create empty Spotify playlist + const createPlaylistUrl = 'https://api.spotify.com/v1/me/playlists'; const createPlaylistBody = JSON.stringify({ name: tripName, - description: `Roadtrip from ${origin} to ${destination}. Themes: ${searchQueries - .slice(0, 8) - .join(", ")}`, + description: `Roadtrip from ${origin} to ${destination}${cleanGenre ? ` · ${cleanGenre} vibes` : ''}.`, public: false, }); - console.log("CREATE_PLAYLIST_URL:", createPlaylistUrl); - console.log("CREATE_PLAYLIST_BODY:", createPlaylistBody); + console.log('CREATE_PLAYLIST_URL:', createPlaylistUrl); const createPlaylistRes = await fetch(createPlaylistUrl, { - method: "POST", + method: 'POST', headers: { Authorization: `Bearer ${providerToken}`, - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: createPlaylistBody, }); - console.log("CREATE_PLAYLIST_HTTP_STATUS:", createPlaylistRes.status); - + 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) { await clearSpotifyTokens(); - - 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." - ); - + 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"; - - 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)}` + `Spotify API returned status ${createPlaylistRes.status} for CreatePlaylist: ${createPlaylistResText.substring(0, 150)}` ); } let playlistData: any; - try { playlistData = JSON.parse(createPlaylistResText); } catch { - throw new Error( - `Failed to parse JSON response for CreatePlaylist: ${createPlaylistResText.substring( - 0, - 150 - )}` - ); + throw new Error(`Failed to parse JSON 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 varied tracks based on duration - console.log("TARGET_PLAYLIST_DURATION_MS:", tripDurationMs); + console.log('TARGET_PLAYLIST_DURATION_MS:', tripDurationMs); - let accumulatedDurationMs = 0; + // ─── PHASE 1: Collect style tracks (80% of target, genre-validated) ───────── const selectedTracks: SelectedSpotifyTrack[] = []; - const selectedTrackIds = new Set(); - const artistCount = new Map(); - let searchRequestsCount = 0; - let queryIndex = 0; + const allStyleQueries = [...styleQueries, ...aiArtistQueries]; + console.log('[Playlist] Phase 1 style queries:', allStyleQueries); - const MAX_SEARCH_REQUESTS = 40; - const MAX_TRACKS = 400; - const MAX_TRACKS_PER_ARTIST = 3; + for (const query of allStyleQueries) { + if (selectedTracks.length >= styleTargetCount) break; + if (accumulatedDurationMs >= tripDurationMs) break; + if (searchRequestsCount >= 60) break; - while ( - accumulatedDurationMs < tripDurationMs && - searchRequestsCount < MAX_SEARCH_REQUESTS && - selectedTracks.length < MAX_TRACKS && - searchQueries.length > 0 - ) { - const currentQuery = searchQueries[queryIndex % searchQueries.length]; - const queryEncoded = encodeURIComponent(currentQuery); - 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", - }, - }); - - 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) - ); - - 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 shuffledTracks) { - if (selectedTracks.length >= MAX_TRACKS) break; + const tracks = await searchAndFilter(query, true); + for (const t of tracks) { + if (selectedTracks.length >= styleTargetCount) break; if (accumulatedDurationMs >= tripDurationMs) break; + if (selectedTrackIds.has(t.id)) continue; - const trackId = track.id; - const trackUri = track.uri; - const trackDurationMs = track.duration_ms; + const artistKey = getMainArtistKey(t as any); + if ((artistCount.get(artistKey) ?? 0) >= MAX_TRACKS_PER_ARTIST) continue; - 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++; + selectedTracks.push(t); + selectedTrackIds.add(t.id); + artistCount.set(artistKey, (artistCount.get(artistKey) ?? 0) + 1); + accumulatedDurationMs += t.duration_ms; } - - console.log("TRACKS_AFTER_FILTER_COUNT:", tracksAfterFilter); - 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); - console.log("SELECTED_TRACKS_TOTAL_DURATION_MS:", accumulatedDurationMs); + console.log('[Playlist] After Phase 1 – styleTracks count:', selectedTracks.length, '| duration (ms):', accumulatedDurationMs); + + // ─── PHASE 2: Fill remaining duration with roadtrip filler (no genre check) ─ + if (!hasGenre && accumulatedDurationMs < tripDurationMs) { + const fillerQueries = [...aiArtistQueries, ...genericFillerQueries]; + console.log('[Playlist] Phase 2 filler queries:', fillerQueries); + + for (const query of fillerQueries) { + if (accumulatedDurationMs >= tripDurationMs) break; + if (searchRequestsCount >= 60) break; + + const tracks = await searchAndFilter(query, false); + for (const t of tracks) { + if (accumulatedDurationMs >= tripDurationMs) break; + if (selectedTrackIds.has(t.id)) continue; + + const artistKey = getMainArtistKey(t as any); + if ((artistCount.get(artistKey) ?? 0) >= MAX_TRACKS_PER_ARTIST) continue; + + selectedTracks.push(t); + selectedTrackIds.add(t.id); + artistCount.set(artistKey, (artistCount.get(artistKey) ?? 0) + 1); + accumulatedDurationMs += t.duration_ms; + } + } + + console.log('[Playlist] After Phase 2 – total tracks:', selectedTracks.length, '| duration (ms):', accumulatedDurationMs); + } + + console.log('[Playlist] finalTracks:', selectedTracks.length); + console.log('[SpotifyPlaylistsDebug] Favorite Genre Received:', favoriteGenre || '(empty)'); + console.log('[SpotifyPlaylistsDebug] Normalized Genre:', cleanGenre || '(empty)'); + console.log('[SpotifyPlaylistsDebug] Target Duration (ms):', tripDurationMs); + console.log('[SpotifyPlaylistsDebug] Raw Tracks Found (Accumulated):', totalRawResultsCount); + console.log('[SpotifyPlaylistsDebug] Tracks Rejected by Genre:', tracksRejectedCount); + console.log('[SpotifyPlaylistsDebug] Final Tracks Added:', selectedTracks.length); + console.log('[SpotifyPlaylistsDebug] Final Playlist Duration (ms):', accumulatedDurationMs); if (selectedTracks.length > 0) { // F. Add tracks to playlist in chunks @@ -655,18 +715,27 @@ Example: ["rock road trip", "rock hits", "porto travel songs", "portuguese indie 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.`; + if (hasGenre) { + // Accept a tolerance of about 5-10 minutes (300,000 to 600,000 ms) below trip duration + if (accumulatedDurationMs < tripDurationMs - 300000) { + playlistSuccessMessage = "A playlist foi criada apenas com músicas do estilo escolhido, mas ficou mais curta porque não foram encontradas músicas suficientes."; } else { - const minutes = Math.round(accumulatedDurationMs / 60000); - playlistSuccessMessage = `Viagem guardada! Playlist criada com ${minutes} minutos de música.`; + playlistSuccessMessage = "Viagem e playlist criadas com sucesso apenas com músicas do estilo escolhido!"; + } + } else { + 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.`; + } } } } @@ -706,12 +775,20 @@ Example: ["rock road trip", "rock hits", "porto travel songs", "portuguese indie } else { if (playlistCreationFailed) { if (playlistFailureReason === 'notracks') { - Alert.alert('Sucesso!', 'Playlist criada, mas não foram encontradas músicas para adicionar.'); + if (hasGenre) { + Alert.alert('Aviso', 'Não foi possível encontrar músicas suficientes desse estilo. Tenta outro estilo musical.'); + } else { + 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); + if (hasGenre && accumulatedDurationMs < tripDurationMs - 300000) { + Alert.alert('Aviso', playlistSuccessMessage); + } else { + Alert.alert('Sucesso!', playlistSuccessMessage); + } } else { Alert.alert('Sucesso!', 'Viagem calculada e guardada!'); }