Save current app updates

This commit is contained in:
2026-05-28 23:55:35 +01:00
parent d85e327c07
commit ad8042adaa
2 changed files with 535 additions and 206 deletions

View File

@@ -408,9 +408,7 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
scrollContent: { scrollContent: {
flexGrow: 1, paddingTop: 48,
justifyContent: 'space-between',
paddingTop: 60,
}, },
headerContainer: { headerContainer: {
alignItems: 'center', alignItems: 'center',
@@ -460,12 +458,8 @@ const styles = StyleSheet.create({
width: '100%', width: '100%',
borderTopLeftRadius: 32, borderTopLeftRadius: 32,
borderTopRightRadius: 32, borderTopRightRadius: 32,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
padding: 24, padding: 24,
paddingBottom: Platform.OS === 'ios' ? 40 : 24, paddingBottom: Platform.OS === 'ios' ? 40 : 32,
flexGrow: 1,
justifyContent: 'flex-start',
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: -4 }, shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1, shadowOpacity: 0.1,

View File

@@ -6,6 +6,85 @@ import { colors } from '../../utils/colors';
import { supabase } from '../../services/supabase'; import { supabase } from '../../services/supabase';
import { getSpotifyAccessToken, refreshSpotifyToken, clearSpotifyTokens } from '../../auth/spotifyToken'; import { getSpotifyAccessToken, refreshSpotifyToken, clearSpotifyTokens } from '../../auth/spotifyToken';
import { OLLAMA_API_URL } from '../../services/ollama'; 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 // @ts-ignore
export default function NewTripScreen({ navigation }) { export default function NewTripScreen({ navigation }) {
@@ -81,35 +160,35 @@ export default function NewTripScreen({ navigation }) {
console.log("SPOTIFY_ACCESS_TOKEN_EXISTS:", !!providerToken); console.log("SPOTIFY_ACCESS_TOKEN_EXISTS:", !!providerToken);
if (providerToken) { if (providerToken) {
// Validate token via GET /v1/me (free endpoint, no Premium required) // Validate token via GET /v1/me (free endpoint, no Premium required)
let meRes = await fetch('https://api.spotify.com/v1/me', { let meRes = await fetch('https://api.spotify.com/v1/me', {
headers: { Authorization: `Bearer ${providerToken}` } headers: { Authorization: `Bearer ${providerToken}` }
}); });
if (meRes.status === 401) { if (meRes.status === 401) {
console.log("Spotify token is invalid/expired (401), attempting to refresh..."); console.log("Spotify token is invalid/expired (401), attempting to refresh...");
const newToken = await refreshSpotifyToken(); const newToken = await refreshSpotifyToken();
if (newToken) { if (newToken) {
providerToken = newToken; providerToken = newToken;
console.log("Spotify token refreshed successfully!"); console.log("Spotify token refreshed successfully!");
} else { } else {
console.log("Failed to refresh Spotify token."); console.log("Failed to refresh Spotify token.");
providerToken = null; providerToken = null;
} }
} else if (!meRes.ok) { } else if (!meRes.ok) {
const meErr = await meRes.text(); const meErr = await meRes.text();
console.warn("Spotify GET /v1/me failed:", meRes.status, meErr); console.warn("Spotify GET /v1/me failed:", meRes.status, meErr);
providerToken = null; providerToken = null;
} else { } else {
console.log("Spotify token valid (GET /v1/me returned 200 OK)."); console.log("Spotify token valid (GET /v1/me returned 200 OK).");
} }
} }
if (!providerToken) { if (!providerToken) {
console.log("Spotify token missing or expired, skipping playlist generation."); console.log("Spotify token missing or expired, skipping playlist generation.");
playlistCreationFailed = true; playlistCreationFailed = true;
playlistFailureReason = 'token'; playlistFailureReason = 'token';
Alert.alert('Spotify Desligado', 'O token do Spotify expirou ou está em falta. Por favor reconecte o Spotify no Perfil.'); Alert.alert('Spotify Desligado', 'O token do Spotify expirou ou está em falta. Por favor reconecte o Spotify no Perfil.');
} else { } else {
// B. Fetch Spotify User ID (reuse /v1/me — already validated above) // B. Fetch Spotify User ID (reuse /v1/me — already validated above)
const spotifyUserRes = await fetch('https://api.spotify.com/v1/me', { const spotifyUserRes = await fetch('https://api.spotify.com/v1/me', {
@@ -124,173 +203,408 @@ export default function NewTripScreen({ navigation }) {
console.log("SPOTIFY_USER_ID_EXISTS:", !!spotifyUserId); console.log("SPOTIFY_USER_ID_EXISTS:", !!spotifyUserId);
if (!spotifyUserId) throw new Error('Could not fetch Spotify User ID from /v1/me'); if (!spotifyUserId) throw new Error('Could not fetch Spotify User ID from /v1/me');
// C. Call Ollama server // C. Build varied music queries using AI + favorite genre
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.`; function shuffleArray<T>(array: T[]): T[] {
const ollamaRes = await fetch(`${OLLAMA_API_URL}/api/chat`, { const copy = [...array];
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 for (let i = copy.length - 1; i > 0; i--) {
try { const j = Math.floor(Math.random() * (i + 1));
const ollamaData = await safeParseJson(ollamaRes, 'Ollama'); [copy[i], copy[j]] = [copy[j], copy[i]];
let rawAiText = ollamaData?.message?.content || ""; }
// Clean AI text return copy;
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);
} }
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<string> {
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 // 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({ const createPlaylistBody = JSON.stringify({
name: tripName, name: tripName,
description: `Roadtrip from ${origin} to ${destination}. Themes: ${searchQueries.join(', ')}`, description: `Roadtrip from ${origin} to ${destination}. Themes: ${searchQueries
public: false .slice(0, 8)
.join(", ")}`,
public: false,
}); });
console.log("CREATE_PLAYLIST_URL:", createPlaylistUrl); console.log("CREATE_PLAYLIST_URL:", createPlaylistUrl);
console.log("CREATE_PLAYLIST_BODY:", createPlaylistBody); 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("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) { if (createPlaylistRes.status === 403) {
// Stored refresh token predates playlist scopes — clear tokens so next login forces full re-auth
await clearSpotifyTokens(); await clearSpotifyTokens();
console.warn("CREATE_PLAYLIST_403: Cleared stale Spotify tokens. User must reconnect.");
Alert.alert( console.warn(
'Permissão Spotify Necessária', "CREATE_PLAYLIST_403: Cleared stale Spotify tokens. User must reconnect."
'Reconnect Spotify to grant playlist permissions. Go to Profile and log in with Spotify again.'
); );
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";
// Skip further playlist work — trip will still be saved
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(`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; let playlistData: any;
try { try {
playlistData = JSON.parse(createPlaylistResText); playlistData = JSON.parse(createPlaylistResText);
} catch (e) { } catch {
throw new Error(`Failed to parse JSON response for CreatePlaylist: ${createPlaylistResText.substring(0, 150)}`); 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; const playlistId = playlistData.id;
generatedPlaylistUrl = playlistData.external_urls.spotify; 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); console.log("TARGET_PLAYLIST_DURATION_MS:", tripDurationMs);
let accumulatedDurationMs = 0; let accumulatedDurationMs = 0;
let selectedTracks: { id: string; uri: string; duration_ms: number }[] = []; const selectedTracks: SelectedSpotifyTrack[] = [];
const selectedTrackIds = new Set<string>();
const artistCount = new Map<string, number>();
let searchRequestsCount = 0; let searchRequestsCount = 0;
let queryIndex = 0;
const MAX_SEARCH_REQUESTS = 40; const MAX_SEARCH_REQUESTS = 40;
const MAX_TRACKS = 400; const MAX_TRACKS = 400;
let queryIndex = 0; const MAX_TRACKS_PER_ARTIST = 3;
let offset = 0;
let noMoreTracks = false;
while ( while (
accumulatedDurationMs < tripDurationMs && accumulatedDurationMs < tripDurationMs &&
searchRequestsCount < MAX_SEARCH_REQUESTS && searchRequestsCount < MAX_SEARCH_REQUESTS &&
selectedTracks.length < MAX_TRACKS && selectedTracks.length < MAX_TRACKS &&
!noMoreTracks searchQueries.length > 0
) { ) {
const currentQuery = searchQueries[queryIndex % searchQueries.length]; const currentQuery = searchQueries[queryIndex % searchQueries.length];
const queryEncoded = encodeURIComponent(currentQuery); const queryEncoded = encodeURIComponent(currentQuery);
const searchUrl = `https://api.spotify.com/v1/search?type=track&q=${queryEncoded}&limit=10&market=${spotifyUserCountry}&offset=${offset}`; const offset = getRandomSpotifyOffset();
console.log("TRACK_SEARCH_QUERY:", currentQuery, "offset:", offset); 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); console.log("TRACK_SEARCH_URL:", searchUrl);
const searchRes = await fetch(searchUrl, { const searchRes = await fetch(searchUrl, {
headers: { headers: {
'Authorization': `Bearer ${providerToken}`, Authorization: `Bearer ${providerToken}`,
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}); });
console.log("TRACK_SEARCH_STATUS:", searchRes.status); console.log("TRACK_SEARCH_STATUS:", searchRes.status);
if (!searchRes.ok) { if (!searchRes.ok) {
const errText = await searchRes.text(); 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'); console.warn(
const tracks = searchData?.tracks?.items || []; "Spotify search failed:",
console.log("TRACKS_RAW_FOUND_COUNT:", tracks.length); searchRes.status,
errText.substring(0, 150)
);
if (tracks.length === 0) { console.log(
queryIndex++; "TRACK_SEARCH_RESPONSE_BODY_IF_FAILED:",
offset = 0; errText.substring(0, 300)
if (queryIndex >= searchQueries.length * 3) { );
noMoreTracks = true;
} searchRequestsCount++;
searchRequestsCount++; queryIndex++;
continue; 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; let tracksAfterFilter = 0;
for (const track of tracks) { for (const track of shuffledTracks) {
if (selectedTracks.length >= MAX_TRACKS) break; if (selectedTracks.length >= MAX_TRACKS) break;
if (accumulatedDurationMs >= tripDurationMs) break; if (accumulatedDurationMs >= tripDurationMs) break;
if (track.id && track.uri && track.duration_ms && track.is_local !== true) { const trackId = track.id;
if (track.is_playable === undefined || track.is_playable !== false) { const trackUri = track.uri;
if (!selectedTracks.some(t => t.id === track.id)) { const trackDurationMs = track.duration_ms;
selectedTracks.push({ id: track.id, uri: track.uri, duration_ms: track.duration_ms });
accumulatedDurationMs += track.duration_ms; if (!trackId) continue;
tracksAfterFilter++; 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); 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);
offset += 10;
if (offset >= 1000) {
queryIndex++;
offset = 0;
}
searchRequestsCount++; searchRequestsCount++;
queryIndex++;
} }
console.log("TRACKS_SELECTED_COUNT:", selectedTracks.length); console.log("TRACKS_SELECTED_COUNT:", selectedTracks.length);
@@ -298,48 +612,69 @@ export default function NewTripScreen({ navigation }) {
if (selectedTracks.length > 0) { if (selectedTracks.length > 0) {
// F. Add tracks to playlist in chunks // F. Add tracks to playlist in chunks
const trackUris = selectedTracks.map(t => t.uri); const trackUris = selectedTracks.map((track) => track.uri);
const chunkSize = 100; const chunkSize = 100;
let tracksAddedSuccessfully = true; let tracksAddedSuccessfully = true;
for (let i = 0; i < trackUris.length; i += chunkSize) { for (let i = 0; i < trackUris.length; i += chunkSize) {
const chunk = trackUris.slice(i, i + chunkSize); const chunk = trackUris.slice(i, i + chunkSize);
const addTracksRes = await fetch(`https://api.spotify.com/v1/playlists/${playlistId}/items`, { const addTracksRes = await fetch(
method: 'POST', `https://api.spotify.com/v1/playlists/${playlistId}/items`,
{
method: "POST",
headers: { headers: {
'Authorization': `Bearer ${providerToken}`, Authorization: `Bearer ${providerToken}`,
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({ uris: chunk }) 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)}`);
} }
);
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) { if (tracksAddedSuccessfully) {
console.log("PLAYLIST_CREATE_SUCCESS:", generatedPlaylistUrl); console.log("PLAYLIST_CREATE_SUCCESS:", generatedPlaylistUrl);
playlistCreationFailed = false;
if (accumulatedDurationMs < tripDurationMs - 60000 && (selectedTracks.length >= MAX_TRACKS || searchRequestsCount >= MAX_SEARCH_REQUESTS || noMoreTracks)) { playlistCreationFailed = false;
const hours = Math.round((accumulatedDurationMs / 3600000) * 10) / 10;
if (hours >= 1) { if (
playlistSuccessMessage = `Viagem guardada! Playlist criada com ${hours} horas de música.`; accumulatedDurationMs < tripDurationMs - 60000 &&
} else { (selectedTracks.length >= MAX_TRACKS ||
const minutes = Math.round(accumulatedDurationMs / 60000); searchRequestsCount >= MAX_SEARCH_REQUESTS)
playlistSuccessMessage = `Viagem guardada! Playlist criada com ${minutes} minutos de música.`; ) {
} 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 { } else {
console.warn("No tracks found for queries:", searchQueries); console.warn("No tracks found for queries:", searchQueries);
playlistCreationFailed = true;
playlistFailureReason = 'notracks'; playlistCreationFailed = true;
playlistFailureReason = "notracks";
} }
} }
} catch (playlistError: any) { } catch (playlistError: any) {
@@ -366,24 +701,24 @@ export default function NewTripScreen({ navigation }) {
}); });
if (dbError) { if (dbError) {
console.error("DB Insert error:", 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); Alert.alert('Erro ao Guardar', 'Não foi possível guardar a viagem na base de dados: ' + dbError.message);
} 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.'); 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); Alert.alert('Sucesso!', playlistSuccessMessage);
} else { } else {
Alert.alert('Sucesso!', 'Viagem calculada e guardada!'); Alert.alert('Sucesso!', 'Viagem calculada e guardada!');
} }
navigation.goBack(); navigation.goBack();
} }
} catch (dbEx) { } catch (dbEx) {
console.error("Exception during DB save:", dbEx); console.error("Exception during DB save:", dbEx);
} }
} else { } else {