WIP playlist style fix

This commit is contained in:
Eduardo Silva
2026-06-12 12:10:05 +01:00
parent b502252811
commit 34dd03b1f6

View File

@@ -84,6 +84,62 @@ function getRouteWaypoints(
return waypoints; return waypoints;
} }
const artistGenreCache = new Map<string, string[]>();
async function getArtistGenres(artistId: string, token: string): Promise<string[]> {
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<string, string[]> = {
'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 // @ts-ignore
@@ -127,6 +183,8 @@ export default function NewTripScreen({ navigation }) {
let playlistCreationFailed = false; let playlistCreationFailed = false;
let playlistFailureReason = ''; let playlistFailureReason = '';
let playlistSuccessMessage = 'Viagem e playlist criadas com sucesso.'; let playlistSuccessMessage = 'Viagem e playlist criadas com sucesso.';
let hasGenre = false;
let accumulatedDurationMs = 0;
try { try {
console.log("GENERATING_PLAYLIST_FOR_TRIP:", tripName); console.log("GENERATING_PLAYLIST_FOR_TRIP:", tripName);
@@ -304,311 +362,313 @@ export default function NewTripScreen({ navigation }) {
console.log("PLAYLIST_FAVORITE_GENRE_USED:", favoriteGenre || "none"); console.log("PLAYLIST_FAVORITE_GENRE_USED:", favoriteGenre || "none");
const ollamaPrompt = `I am taking a roadtrip from ${origin} to ${destination}. hasGenre = Boolean(favoriteGenre && favoriteGenre.trim().length > 0);
The trip is called "${tripName}" and takes about ${duration}. const cleanGenre = hasGenre ? favoriteGenre.trim().toLowerCase() : "";
The user's favorite music genre is: "${favoriteGenre || "not set"}".
Reply ONLY with a JSON array of up to 10 Spotify search queries. console.log('[Playlist] favoriteMusicStyle:', favoriteGenre || '(empty)');
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[] = []; // ── Style → curated query map (artist-based; genre: filter unreliable for tracks)
const STYLE_QUERY_MAP: Record<string, string[]> = {
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 { try {
const ollamaRes = await fetch(`${OLLAMA_API_URL}/api/chat`, { const ollamaRes = await fetch(`${OLLAMA_API_URL}/api/chat`, {
method: "POST", method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
model: "qwen3-coder:30b", model: 'qwen3-coder:30b',
messages: [{ role: "user", content: ollamaPrompt }], messages: [{ role: 'user', content: ollamaPrompt }],
stream: false, stream: false,
}), }),
}); });
const ollamaData = await safeParseJson(ollamaRes, 'Ollama');
const ollamaData = await safeParseJson(ollamaRes, "Ollama"); let rawAiText = (ollamaData?.message?.content || '')
let rawAiText = ollamaData?.message?.content || ""; .replace(/```json/g, '').replace(/```/g, '').trim();
if (rawAiText.startsWith('[')) {
rawAiText = rawAiText
.replace(/```json/g, "")
.replace(/```/g, "")
.trim();
if (rawAiText.length > 0 && rawAiText.startsWith("[")) {
const parsed = JSON.parse(rawAiText); const parsed = JSON.parse(rawAiText);
if (Array.isArray(parsed)) { if (Array.isArray(parsed)) {
aiQueries = parsed aiArtistQueries = parsed.map(String).map(cleanSearchQuery).filter(Boolean).slice(0, 10);
.map(String)
.map(cleanSearchQuery)
.filter(Boolean)
.slice(0, 10);
} }
} }
} catch (aiError) { } catch {
console.log("AI parsing failed, using fallback queries.", aiError); console.log('[Playlist] AI query generation failed; continuing without it.');
} }
const favoriteGenreQueries = favoriteGenre // Generic roadtrip filler queries (only used in Phase 2 when no genre, or to pad when genre exhausted)
? [ const genericFillerQueries = [
`${favoriteGenre} road trip`, `${destination} road trip`, `${origin} to ${destination} music`,
`${favoriteGenre} hits`, 'road trip hits', 'travel songs', 'driving music',
`${favoriteGenre} travel songs`, 'summer hits', 'feel good road trip', 'top hits Portugal',
`${favoriteGenre} driving music`, ].map(cleanSearchQuery).filter(Boolean);
`${favoriteGenre} playlist`,
`${favoriteGenre} ${destination}`,
]
: [];
const tripSpecificQueries = [ // ── Target track count (based on trip duration, ~4 min per song)
`${destination} road trip`, const tripDurationMinutes = tripDurationMs / 60000;
`${origin} to ${destination} music`, const TARGET_TRACK_COUNT = Math.max(10, Math.ceil(tripDurationMinutes / 4));
`${tripName} playlist`, const STYLE_RATIO = 0.80;
`${destination} travel songs`, const styleTargetCount = hasGenre ? Math.ceil(TARGET_TRACK_COUNT * STYLE_RATIO) : 0;
`${origin} ${destination} road trip`,
];
const fallbackQueries = [ console.log('[Playlist] targetTrackCount:', TARGET_TRACK_COUNT);
"road trip songs", console.log('[Playlist] styleTargetCount:', styleTargetCount);
"travel songs", console.log('[Playlist] styleQueries:', styleQueries);
"summer hits",
"top hits Portugal",
"pop hits",
"driving music",
"feel good road trip",
"european travel music",
"party road trip",
"indie road trip",
];
const firstQueries = [ const MAX_TRACKS_PER_ARTIST = 3;
...favoriteGenreQueries,
...aiQueries,
]
.map(cleanSearchQuery)
.filter(Boolean);
const remainingQueries = [ // Helper to run Spotify track search for a given query and return accepted tracks
...tripSpecificQueries, const selectedTrackIds = new Set<string>();
...fallbackQueries, const artistCount = new Map<string, number>();
] let totalRawResultsCount = 0;
.map(cleanSearchQuery) let tracksRejectedCount = 0;
.filter(Boolean); let searchRequestsCount = 0;
const searchQueries = Array.from( const searchAndFilter = async (
new Set([ query: string,
...firstQueries, validateGenre: boolean,
...shuffleArray(remainingQueries), ): Promise<SelectedSpotifyTrack[]> => {
]) 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() const queryEncoded = encodeURIComponent(query);
.toString(36) const searchUrl =
.slice(2)}`; `https://api.spotify.com/v1/search?type=track` +
`&q=${queryEncoded}&limit=20&market=${spotifyUserCountry}&offset=${offset}`;
console.log("PLAYLIST_RANDOM_SEED:", playlistRandomSeed); console.log('TRACK_SEARCH_QUERY:', query, '| offset:', offset);
console.log("PLAYLIST_MUSIC_QUERIES:", searchQueries);
// D. Create empty playlist const searchRes = await fetch(searchUrl, {
const createPlaylistUrl = "https://api.spotify.com/v1/me/playlists"; 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({ const createPlaylistBody = JSON.stringify({
name: tripName, name: tripName,
description: `Roadtrip from ${origin} to ${destination}. Themes: ${searchQueries description: `Roadtrip from ${origin} to ${destination}${cleanGenre ? ` · ${cleanGenre} vibes` : ''}.`,
.slice(0, 8)
.join(", ")}`,
public: false, public: false,
}); });
console.log("CREATE_PLAYLIST_URL:", createPlaylistUrl); console.log('CREATE_PLAYLIST_URL:', createPlaylistUrl);
console.log("CREATE_PLAYLIST_BODY:", createPlaylistBody);
const createPlaylistRes = await fetch(createPlaylistUrl, { const createPlaylistRes = await fetch(createPlaylistUrl, {
method: "POST", method: 'POST',
headers: { headers: {
Authorization: `Bearer ${providerToken}`, Authorization: `Bearer ${providerToken}`,
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
body: createPlaylistBody, body: createPlaylistBody,
}); });
console.log("CREATE_PLAYLIST_HTTP_STATUS:", createPlaylistRes.status); console.log('CREATE_PLAYLIST_HTTP_STATUS:', createPlaylistRes.status);
const createPlaylistResText = await createPlaylistRes.text(); const createPlaylistResText = await createPlaylistRes.text();
if (!createPlaylistRes.ok) { if (!createPlaylistRes.ok) {
console.log( console.log('CREATE_PLAYLIST_RESPONSE_BODY_IF_FAILED:', createPlaylistResText.substring(0, 300));
"CREATE_PLAYLIST_RESPONSE_BODY_IF_FAILED:",
createPlaylistResText.substring(0, 300)
);
if (createPlaylistRes.status === 403) { if (createPlaylistRes.status === 403) {
await clearSpotifyTokens(); await clearSpotifyTokens();
console.warn('CREATE_PLAYLIST_403: Cleared stale Spotify tokens. User must reconnect.');
console.warn( Alert.alert('Permissão Spotify Necessária', 'Reconecta o Spotify para dar permissão de criar playlists.');
"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; playlistCreationFailed = true;
playlistFailureReason = "scope"; playlistFailureReason = 'scope';
throw new Error('CREATE_PLAYLIST_SCOPE_ERROR: Tokens cleared, re-auth required.');
throw new Error(
"CREATE_PLAYLIST_SCOPE_ERROR: Tokens cleared, re-auth required."
);
} }
throw new Error( throw new Error(
`Spotify API returned status ${createPlaylistRes.status `Spotify API returned status ${createPlaylistRes.status} for CreatePlaylist: ${createPlaylistResText.substring(0, 150)}`
} for CreatePlaylist: ${createPlaylistResText.substring(0, 150)}`
); );
} }
let playlistData: any; let playlistData: any;
try { try {
playlistData = JSON.parse(createPlaylistResText); playlistData = JSON.parse(createPlaylistResText);
} catch { } catch {
throw new Error( throw new Error(`Failed to parse JSON for CreatePlaylist: ${createPlaylistResText.substring(0, 150)}`);
`Failed to parse JSON response for CreatePlaylist: ${createPlaylistResText.substring(
0,
150
)}`
);
} }
if (!playlistData.id) { if (!playlistData.id) throw new Error('Could not create playlist');
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. 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 selectedTracks: SelectedSpotifyTrack[] = [];
const selectedTrackIds = new Set<string>();
const artistCount = new Map<string, number>();
let searchRequestsCount = 0; const allStyleQueries = [...styleQueries, ...aiArtistQueries];
let queryIndex = 0; console.log('[Playlist] Phase 1 style queries:', allStyleQueries);
const MAX_SEARCH_REQUESTS = 40; for (const query of allStyleQueries) {
const MAX_TRACKS = 400; if (selectedTracks.length >= styleTargetCount) break;
const MAX_TRACKS_PER_ARTIST = 3; if (accumulatedDurationMs >= tripDurationMs) break;
if (searchRequestsCount >= 60) break;
while ( const tracks = await searchAndFilter(query, true);
accumulatedDurationMs < tripDurationMs && for (const t of tracks) {
searchRequestsCount < MAX_SEARCH_REQUESTS && if (selectedTracks.length >= styleTargetCount) break;
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<SpotifySearchTrack>(rawTracks);
let tracksAfterFilter = 0;
for (const track of shuffledTracks) {
if (selectedTracks.length >= MAX_TRACKS) break;
if (accumulatedDurationMs >= tripDurationMs) break; if (accumulatedDurationMs >= tripDurationMs) break;
if (selectedTrackIds.has(t.id)) continue;
const trackId = track.id; const artistKey = getMainArtistKey(t as any);
const trackUri = track.uri; if ((artistCount.get(artistKey) ?? 0) >= MAX_TRACKS_PER_ARTIST) continue;
const trackDurationMs = track.duration_ms;
if (!trackId) continue; selectedTracks.push(t);
if (!trackUri) continue; selectedTrackIds.add(t.id);
if (!trackDurationMs) continue; artistCount.set(artistKey, (artistCount.get(artistKey) ?? 0) + 1);
if (track.is_local === true) continue; accumulatedDurationMs += t.duration_ms;
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);
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('[Playlist] After Phase 1 styleTracks count:', selectedTracks.length, '| duration (ms):', accumulatedDurationMs);
console.log("SELECTED_TRACKS_TOTAL_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) { if (selectedTracks.length > 0) {
// F. Add tracks to playlist in chunks // F. Add tracks to playlist in chunks
@@ -655,18 +715,27 @@ Example: ["rock road trip", "rock hits", "porto travel songs", "portuguese indie
playlistCreationFailed = false; playlistCreationFailed = false;
if ( if (hasGenre) {
accumulatedDurationMs < tripDurationMs - 60000 && // Accept a tolerance of about 5-10 minutes (300,000 to 600,000 ms) below trip duration
(selectedTracks.length >= MAX_TRACKS || if (accumulatedDurationMs < tripDurationMs - 300000) {
searchRequestsCount >= MAX_SEARCH_REQUESTS) playlistSuccessMessage = "A playlist foi criada apenas com músicas do estilo escolhido, mas ficou mais curta porque não foram encontradas músicas suficientes.";
) {
const hours = Math.round((accumulatedDurationMs / 3600000) * 10) / 10;
if (hours >= 1) {
playlistSuccessMessage = `Viagem guardada! Playlist criada com ${hours} horas de música.`;
} else { } else {
const minutes = Math.round(accumulatedDurationMs / 60000); playlistSuccessMessage = "Viagem e playlist criadas com sucesso apenas com músicas do estilo escolhido!";
playlistSuccessMessage = `Viagem guardada! Playlist criada com ${minutes} minutos de música.`; }
} 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 { } else {
if (playlistCreationFailed) { if (playlistCreationFailed) {
if (playlistFailureReason === 'notracks') { 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 { } else {
Alert.alert('Sucesso!', 'Viagem guardada! A playlist do Spotify não pôde ser criada, mas a viagem foi guardada.'); Alert.alert('Sucesso!', 'Viagem guardada! A playlist do Spotify não pôde ser criada, mas a viagem foi guardada.');
} }
} else if (generatedPlaylistUrl) { } else if (generatedPlaylistUrl) {
Alert.alert('Sucesso!', playlistSuccessMessage); if (hasGenre && accumulatedDurationMs < tripDurationMs - 300000) {
Alert.alert('Aviso', playlistSuccessMessage);
} else {
Alert.alert('Sucesso!', playlistSuccessMessage);
}
} else { } else {
Alert.alert('Sucesso!', 'Viagem calculada e guardada!'); Alert.alert('Sucesso!', 'Viagem calculada e guardada!');
} }