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) {
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 createPlaylistBody = JSON.stringify({
name: tripName,
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",
headers: {
Authorization: `Bearer ${providerToken}`,
"Content-Type": "application/json",
},
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)
);
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."
);
playlistCreationFailed = true;
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)}`
);
}
let playlistData: any;
try {
playlistData = JSON.parse(createPlaylistResText);
} catch { } catch {
throw new Error( console.log('[Playlist] AI query generation failed; continuing without it.');
`Failed to parse JSON response for CreatePlaylist: ${createPlaylistResText.substring(
0,
150
)}`
);
} }
if (!playlistData.id) { // Generic roadtrip filler queries (only used in Phase 2 when no genre, or to pad when genre exhausted)
throw new Error("Could not create playlist"); 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 playlistId = playlistData.id; // ── Target track count (based on trip duration, ~4 min per song)
generatedPlaylistUrl = playlistData.external_urls.spotify; 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;
// E. Fill playlist with varied tracks based on duration console.log('[Playlist] targetTrackCount:', TARGET_TRACK_COUNT);
console.log("TARGET_PLAYLIST_DURATION_MS:", tripDurationMs); console.log('[Playlist] styleTargetCount:', styleTargetCount);
console.log('[Playlist] styleQueries:', styleQueries);
let accumulatedDurationMs = 0;
const selectedTracks: SelectedSpotifyTrack[] = [];
const selectedTrackIds = new Set<string>();
const artistCount = new Map<string, number>();
let searchRequestsCount = 0;
let queryIndex = 0;
const MAX_SEARCH_REQUESTS = 40;
const MAX_TRACKS = 400;
const MAX_TRACKS_PER_ARTIST = 3; const MAX_TRACKS_PER_ARTIST = 3;
while ( // Helper to run Spotify track search for a given query and return accepted tracks
accumulatedDurationMs < tripDurationMs && const selectedTrackIds = new Set<string>();
searchRequestsCount < MAX_SEARCH_REQUESTS && const artistCount = new Map<string, number>();
selectedTracks.length < MAX_TRACKS && let totalRawResultsCount = 0;
searchQueries.length > 0 let tracksRejectedCount = 0;
) { let searchRequestsCount = 0;
const currentQuery = searchQueries[queryIndex % searchQueries.length];
const queryEncoded = encodeURIComponent(currentQuery);
const offset = getRandomSpotifyOffset();
const searchAndFilter = async (
query: string,
validateGenre: boolean,
): 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 queryEncoded = encodeURIComponent(query);
const searchUrl = const searchUrl =
`https://api.spotify.com/v1/search` + `https://api.spotify.com/v1/search?type=track` +
`?type=track` + `&q=${queryEncoded}&limit=20&market=${spotifyUserCountry}&offset=${offset}`;
`&q=${queryEncoded}` +
`&limit=10` +
`&market=${spotifyUserCountry}` +
`&offset=${offset}`;
console.log("TRACK_SEARCH_QUERY:", currentQuery); console.log('TRACK_SEARCH_QUERY:', query, '| offset:', offset);
console.log("TRACK_SEARCH_OFFSET:", offset);
console.log("TRACK_SEARCH_URL:", searchUrl);
const searchRes = await fetch(searchUrl, { const searchRes = await fetch(searchUrl, {
headers: { headers: { Authorization: `Bearer ${providerToken}`, 'Content-Type': 'application/json' },
Authorization: `Bearer ${providerToken}`,
"Content-Type": "application/json",
},
}); });
console.log("TRACK_SEARCH_STATUS:", searchRes.status); searchRequestsCount++;
if (!searchRes.ok) { if (!searchRes.ok) {
const errText = await searchRes.text(); const errText = await searchRes.text();
console.warn('[Playlist] Spotify search failed:', searchRes.status, errText.substring(0, 150));
console.warn( break;
"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 searchData = (await safeParseJson(searchRes, 'SearchTracks')) as any;
const rawTracks: SpotifySearchTrack[] = Array.isArray(searchData?.tracks?.items) const rawTracks: SpotifySearchTrack[] = Array.isArray(searchData?.tracks?.items)
? (searchData.tracks.items as SpotifySearchTrack[]) ? searchData.tracks.items : [];
: [];
console.log("TRACKS_RAW_FOUND_COUNT:", rawTracks.length); totalRawResultsCount += rawTracks.length;
console.log(`[Playlist] Query "${query}" offset ${offset}: ${rawTracks.length} raw tracks`);
const shuffledTracks = shuffleArray<SpotifySearchTrack>(rawTracks); for (const track of rawTracks) {
let tracksAfterFilter = 0;
for (const track of shuffledTracks) {
if (selectedTracks.length >= MAX_TRACKS) break;
if (accumulatedDurationMs >= tripDurationMs) break; if (accumulatedDurationMs >= tripDurationMs) break;
const trackId = track.id; const trackId = track.id;
const trackUri = track.uri; const trackUri = track.uri;
const trackDurationMs = track.duration_ms; const trackDurationMs = track.duration_ms;
if (!trackId) continue; if (!trackId || !trackUri || !trackDurationMs) continue;
if (!trackUri) continue;
if (!trackDurationMs) continue;
if (track.is_local === true) continue; if (track.is_local === true) continue;
if (track.is_playable === false) continue; if (track.is_playable === false) continue;
if (selectedTrackIds.has(trackId)) 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); const artistKey = getMainArtistKey(track);
const currentArtistCount = artistCount.get(artistKey) ?? 0; if ((artistCount.get(artistKey) ?? 0) >= MAX_TRACKS_PER_ARTIST) continue;
if (currentArtistCount >= MAX_TRACKS_PER_ARTIST) continue; results.push({ id: trackId, uri: trackUri, duration_ms: trackDurationMs });
}
selectedTracks.push({ // Don't paginate if we already have plenty of tracks from this query
id: trackId, if (rawTracks.length < 20) break;
uri: trackUri, }
duration_ms: trackDurationMs, 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}${cleanGenre ? ` · ${cleanGenre} vibes` : ''}.`,
public: false,
}); });
selectedTrackIds.add(trackId); console.log('CREATE_PLAYLIST_URL:', createPlaylistUrl);
artistCount.set(artistKey, currentArtistCount + 1);
accumulatedDurationMs += trackDurationMs; const createPlaylistRes = await fetch(createPlaylistUrl, {
tracksAfterFilter++; method: 'POST',
headers: {
Authorization: `Bearer ${providerToken}`,
'Content-Type': 'application/json',
},
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));
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.');
playlistCreationFailed = true;
playlistFailureReason = 'scope';
throw new Error('CREATE_PLAYLIST_SCOPE_ERROR: Tokens cleared, re-auth required.');
} }
console.log("TRACKS_AFTER_FILTER_COUNT:", tracksAfterFilter); throw new Error(
console.log("TRACKS_SELECTED_COUNT:", selectedTracks.length); `Spotify API returned status ${createPlaylistRes.status} for CreatePlaylist: ${createPlaylistResText.substring(0, 150)}`
console.log("UNIQUE_ARTISTS_COUNT:", artistCount.size); );
console.log("SELECTED_TRACKS_TOTAL_DURATION_MS:", accumulatedDurationMs);
searchRequestsCount++;
queryIndex++;
} }
console.log("TRACKS_SELECTED_COUNT:", selectedTracks.length); let playlistData: any;
console.log("SELECTED_TRACKS_TOTAL_DURATION_MS:", accumulatedDurationMs); try {
playlistData = JSON.parse(createPlaylistResText);
} catch {
throw new Error(`Failed to parse JSON for CreatePlaylist: ${createPlaylistResText.substring(0, 150)}`);
}
if (!playlistData.id) throw new Error('Could not create playlist');
const playlistId = playlistData.id;
generatedPlaylistUrl = playlistData.external_urls.spotify;
console.log('TARGET_PLAYLIST_DURATION_MS:', tripDurationMs);
// ─── PHASE 1: Collect style tracks (80% of target, genre-validated) ─────────
const selectedTracks: SelectedSpotifyTrack[] = [];
const allStyleQueries = [...styleQueries, ...aiArtistQueries];
console.log('[Playlist] Phase 1 style queries:', allStyleQueries);
for (const query of allStyleQueries) {
if (selectedTracks.length >= styleTargetCount) break;
if (accumulatedDurationMs >= tripDurationMs) break;
if (searchRequestsCount >= 60) 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 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 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) { if (selectedTracks.length > 0) {
// F. Add tracks to playlist in chunks // F. Add tracks to playlist in chunks
@@ -655,6 +715,14 @@ Example: ["rock road trip", "rock hits", "porto travel songs", "portuguese indie
playlistCreationFailed = false; playlistCreationFailed = false;
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 {
playlistSuccessMessage = "Viagem e playlist criadas com sucesso apenas com músicas do estilo escolhido!";
}
} else {
if ( if (
accumulatedDurationMs < tripDurationMs - 60000 && accumulatedDurationMs < tripDurationMs - 60000 &&
(selectedTracks.length >= MAX_TRACKS || (selectedTracks.length >= MAX_TRACKS ||
@@ -670,6 +738,7 @@ Example: ["rock road trip", "rock hits", "porto travel songs", "portuguese indie
} }
} }
} }
}
} else { } else {
console.warn("No tracks found for queries:", searchQueries); console.warn("No tracks found for queries:", searchQueries);
@@ -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') {
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.'); 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) {
if (hasGenre && accumulatedDurationMs < tripDurationMs - 300000) {
Alert.alert('Aviso', playlistSuccessMessage);
} else {
Alert.alert('Sucesso!', playlistSuccessMessage); Alert.alert('Sucesso!', playlistSuccessMessage);
}
} else { } else {
Alert.alert('Sucesso!', 'Viagem calculada e guardada!'); Alert.alert('Sucesso!', 'Viagem calculada e guardada!');
} }