mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-07 19:40:20 +00:00
feat: refactor Google Auth handling and improve media file management with enhanced error logging
This commit is contained in:
1 parent
ad0cc1a7f5
commit
99c0e1186c
3 files changed
+110
-35
No files matched your search
+86
-33
@@ -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) {
|
||||
|
||||
@@ -54,7 +54,12 @@ const PhotoGrid = ({
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{item.local_path ? (
|
||||
<Image source={{ uri: item.local_path }} style={styles.gridImage} resizeMode="cover" />
|
||||
<Image
|
||||
source={{ uri: item.local_path }}
|
||||
style={styles.gridImage}
|
||||
resizeMode="cover"
|
||||
onError={(e) => console.warn(`Image load failed [${item.source_service}]: ${item.local_path?.substring(0, 80)}`, e.nativeEvent?.error)}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.placeholder, { backgroundColor: theme.borderLight }]}>
|
||||
<Text style={{ fontSize: 28 }}>📷</Text>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user