diff --git a/src/AUTH/GoogleAUTH.js b/src/AUTH/GoogleAUTH.js
index c8bb260..ae7f206 100644
--- a/src/AUTH/GoogleAUTH.js
+++ b/src/AUTH/GoogleAUTH.js
@@ -1,7 +1,7 @@
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import * as Application from 'expo-application';
-import * as FileSystem from 'expo-file-system';
+import { File, Directory, Paths } from 'expo-file-system';
import { AuthServiceBase } from './AuthServiceBase';
import { logError } from '../services/ErrorHandler';
@@ -30,7 +30,7 @@ const SCOPES = [
const PICKER_API = 'https://photospicker.googleapis.com/v1';
-const CACHE_DIR = `${FileSystem.cacheDirectory}google_photos/`;
+const CACHE_DIR = new Directory(Paths.cache, 'google_photos');
const REDIRECT_URI = `${Application.applicationId}:/oauthredirect`;
@@ -274,32 +274,55 @@ class GoogleAUTH extends AuthServiceBase {
return { mediaItems: [], nextPageToken: null };
}
- return await res.json();
+ const data = await res.json();
+
+ // Debug: log raw response structure
+ if (data.mediaItems?.length) {
+ console.log('Picker raw item[0]:', JSON.stringify(data.mediaItems[0], null, 2));
+ } else {
+ console.log('Picker returned no mediaItems. Keys:', Object.keys(data));
+ }
+
+ return data;
}
/**
* Normalise Picker API media items to the app's standard format.
*
- * NOTE: The Picker API's baseUrl requires an Authorization header to
- * fetch the image. We store it in `remote_path` / `picker_base_url`
- * so the caller can download it to a local cache file for rendering.
+ * The Picker API response shape per item:
+ * { id, mediaFile: { baseUrl, mimeType, filename, mediaFileMetadata: { ... } } }
+ *
+ * baseUrl requires an Authorization header to fetch the actual image bytes.
*/
_normalisePickerItems(items) {
- return (items || []).map((item) => {
- const file = item.mediaFile || {};
- const isVideo = file.mimeType?.startsWith('video');
+ if (!items?.length) return [];
+
+ return items.map((item, idx) => {
+ // The item may have mediaFile nested, or may be flat
+ const file = item.mediaFile || item;
+ const baseUrl = file.baseUrl || item.baseUrl || null;
+ const filename = file.filename || item.filename || 'Untitled';
+ const mimeType = file.mimeType || item.mimeType || null;
+ const isVideo = mimeType?.startsWith('video');
+ const creationTime = file.mediaFileMetadata?.creationTime
+ || item.mediaFileMetadata?.creationTime
+ || null;
+
+ if (idx === 0) {
+ console.log(`Normalising item[0]: id=${item.id}, baseUrl=${baseUrl?.substring(0, 60)}…, filename=${filename}, mime=${mimeType}`);
+ }
+
return {
source_service: 'GOOGLE_PHOTOS',
- source_id: item.id || file.id,
- title: file.filename || 'Untitled',
+ source_id: item.id || file.id || `picker_${Date.now()}_${idx}`,
+ title: filename,
description: '',
- // local_path is null until we download it with the auth token
local_path: null,
- remote_path: file.baseUrl || '',
- picker_base_url: file.baseUrl || null,
+ remote_path: baseUrl || '',
+ picker_base_url: baseUrl,
media_type: isVideo ? 'video' : 'photo',
- mime_type: file.mimeType || null,
- created_date: file.mediaFileMetadata?.creationTime || null,
+ mime_type: mimeType,
+ created_date: creationTime,
};
});
}
@@ -318,9 +341,18 @@ class GoogleAUTH extends AuthServiceBase {
const token = await this.getAccessToken();
// Ensure cache directory exists
- const dirInfo = await FileSystem.getInfoAsync(CACHE_DIR);
- if (!dirInfo.exists) {
- await FileSystem.makeDirectoryAsync(CACHE_DIR, { intermediates: true });
+ if (!CACHE_DIR.exists) {
+ CACHE_DIR.create({ intermediates: true });
+ } else {
+ // Clean up stale files from previous failed downloads (< 1KB = likely error pages)
+ try {
+ const entries = CACHE_DIR.list();
+ for (const entry of entries) {
+ if (entry instanceof File && entry.size < 1024) {
+ entry.delete();
+ }
+ }
+ } catch (_) { /* ignore cleanup errors */ }
}
const results = [];
@@ -331,31 +363,52 @@ class GoogleAUTH extends AuthServiceBase {
}
try {
- // Use the source_id as a safe filename
const safeId = (item.source_id || 'photo').replace(/[^a-zA-Z0-9_-]/g, '_');
const ext = item.mime_type?.includes('png') ? '.png'
: item.mime_type?.includes('gif') ? '.gif'
: item.mime_type?.includes('webp') ? '.webp'
: '.jpg';
- const localUri = `${CACHE_DIR}${safeId}${ext}`;
+ const destFile = new File(CACHE_DIR, `${safeId}${ext}`);
- // Skip download if already cached
- const fileInfo = await FileSystem.getInfoAsync(localUri);
- if (fileInfo.exists && fileInfo.size > 0) {
- results.push({ ...item, local_path: localUri });
+ // Skip download if already cached and non-empty
+ if (destFile.exists && destFile.size > 1024) {
+ results.push({ ...item, local_path: destFile.uri });
continue;
}
- // Download with auth header — append =d to get the full-res download
- const downloadUrl = `${item.picker_base_url}=d`;
- const download = await FileSystem.downloadAsync(downloadUrl, localUri, {
- headers: { Authorization: `Bearer ${token}` },
- });
+ // The Picker API baseUrl is a direct download link when used with
+ // the Authorization header. No suffix needed.
+ console.log(`Downloading: ${item.title} (${item.picker_base_url.substring(0, 80)}…)`);
- if (download.status === 200) {
- results.push({ ...item, local_path: download.uri });
+ const downloaded = await File.downloadFileAsync(
+ item.picker_base_url,
+ destFile,
+ { headers: { Authorization: `Bearer ${token}` }, idempotent: true }
+ );
+
+ console.log(` → uri=${downloaded.uri}, size=${downloaded.size}`);
+
+ if (downloaded.exists && downloaded.size > 1024) {
+ results.push({ ...item, local_path: downloaded.uri });
+ } else if (downloaded.exists && downloaded.size > 0) {
+ // Suspiciously small — check if it's an error page
+ try {
+ const peek = await downloaded.text();
+ if (peek.startsWith('<') || peek.startsWith('{')) {
+ logError('Download Picker Photo',
+ `Got error page for ${item.title}: ${peek.substring(0, 150)}`);
+ downloaded.delete();
+ results.push(item); // no local_path
+ } else {
+ results.push({ ...item, local_path: downloaded.uri });
+ }
+ } catch (_) {
+ // Can't read as text = probably binary = probably an image
+ results.push({ ...item, local_path: downloaded.uri });
+ }
} else {
- logError('Download Picker Photo', `HTTP ${download.status} for ${item.title}`);
+ logError('Download Picker Photo', `Empty file for ${item.title}`);
+ if (downloaded.exists) downloaded.delete();
results.push(item);
}
} catch (err) {
diff --git a/src/components/PhotoGrid.js b/src/components/PhotoGrid.js
index 4b56f3a..c7b252e 100644
--- a/src/components/PhotoGrid.js
+++ b/src/components/PhotoGrid.js
@@ -54,7 +54,12 @@ const PhotoGrid = ({
activeOpacity={0.8}
>
{item.local_path ? (
-
+ console.warn(`Image load failed [${item.source_service}]: ${item.local_path?.substring(0, 80)}`, e.nativeEvent?.error)}
+ />
) : (
📷
diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js
index 49099f0..a615533 100644
--- a/src/screens/HomeScreen.js
+++ b/src/screens/HomeScreen.js
@@ -8,6 +8,7 @@ import DatabaseService from '../services/DatabaseService';
import DeviceMediaService from '../services/DeviceMediaService';
import GoogleAUTH from '../AUTH/GoogleAUTH';
import NASAUTH from '../AUTH/NASAUTH';
+import { File } from 'expo-file-system';
import TopBar from '../components/TopBar';
import PhotoGrid from '../components/PhotoGrid';
import MediaViewer from '../components/MediaViewer';
@@ -132,7 +133,23 @@ const HomeScreen = ({ navigation, route }) => {
// ─── Previously imported Google Photos (stored in DB) ───
try {
const googleMedia = await DatabaseService.getMediaByService(userId, 'GOOGLE_PHOTOS');
- if (googleMedia?.length) newMedia.push(...googleMedia);
+ if (googleMedia?.length) {
+ // Validate cached files still exist — clear stale paths
+ const validated = googleMedia.map((item) => {
+ if (item.local_path) {
+ try {
+ const f = new File(item.local_path);
+ if (!f.exists || f.size === 0) {
+ return { ...item, local_path: null };
+ }
+ } catch (_) {
+ return { ...item, local_path: null };
+ }
+ }
+ return item;
+ });
+ newMedia.push(...validated);
+ }
} catch (err) {
console.error('DB Google media fetch error:', err);
}