mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-08 03: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 AuthSession from 'expo-auth-session';
|
||||||
import * as WebBrowser from 'expo-web-browser';
|
import * as WebBrowser from 'expo-web-browser';
|
||||||
import * as Application from 'expo-application';
|
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 { AuthServiceBase } from './AuthServiceBase';
|
||||||
import { logError } from '../services/ErrorHandler';
|
import { logError } from '../services/ErrorHandler';
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ const SCOPES = [
|
|||||||
|
|
||||||
const PICKER_API = 'https://photospicker.googleapis.com/v1';
|
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`;
|
const REDIRECT_URI = `${Application.applicationId}:/oauthredirect`;
|
||||||
|
|
||||||
@@ -274,32 +274,55 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
return { mediaItems: [], nextPageToken: null };
|
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.
|
* Normalise Picker API media items to the app's standard format.
|
||||||
*
|
*
|
||||||
* NOTE: The Picker API's baseUrl requires an Authorization header to
|
* The Picker API response shape per item:
|
||||||
* fetch the image. We store it in `remote_path` / `picker_base_url`
|
* { id, mediaFile: { baseUrl, mimeType, filename, mediaFileMetadata: { ... } } }
|
||||||
* so the caller can download it to a local cache file for rendering.
|
*
|
||||||
|
* baseUrl requires an Authorization header to fetch the actual image bytes.
|
||||||
*/
|
*/
|
||||||
_normalisePickerItems(items) {
|
_normalisePickerItems(items) {
|
||||||
return (items || []).map((item) => {
|
if (!items?.length) return [];
|
||||||
const file = item.mediaFile || {};
|
|
||||||
const isVideo = file.mimeType?.startsWith('video');
|
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 {
|
return {
|
||||||
source_service: 'GOOGLE_PHOTOS',
|
source_service: 'GOOGLE_PHOTOS',
|
||||||
source_id: item.id || file.id,
|
source_id: item.id || file.id || `picker_${Date.now()}_${idx}`,
|
||||||
title: file.filename || 'Untitled',
|
title: filename,
|
||||||
description: '',
|
description: '',
|
||||||
// local_path is null until we download it with the auth token
|
|
||||||
local_path: null,
|
local_path: null,
|
||||||
remote_path: file.baseUrl || '',
|
remote_path: baseUrl || '',
|
||||||
picker_base_url: file.baseUrl || null,
|
picker_base_url: baseUrl,
|
||||||
media_type: isVideo ? 'video' : 'photo',
|
media_type: isVideo ? 'video' : 'photo',
|
||||||
mime_type: file.mimeType || null,
|
mime_type: mimeType,
|
||||||
created_date: file.mediaFileMetadata?.creationTime || null,
|
created_date: creationTime,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -318,9 +341,18 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
// Ensure cache directory exists
|
// Ensure cache directory exists
|
||||||
const dirInfo = await FileSystem.getInfoAsync(CACHE_DIR);
|
if (!CACHE_DIR.exists) {
|
||||||
if (!dirInfo.exists) {
|
CACHE_DIR.create({ intermediates: true });
|
||||||
await FileSystem.makeDirectoryAsync(CACHE_DIR, { 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 = [];
|
const results = [];
|
||||||
@@ -331,31 +363,52 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the source_id as a safe filename
|
|
||||||
const safeId = (item.source_id || 'photo').replace(/[^a-zA-Z0-9_-]/g, '_');
|
const safeId = (item.source_id || 'photo').replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
const ext = item.mime_type?.includes('png') ? '.png'
|
const ext = item.mime_type?.includes('png') ? '.png'
|
||||||
: item.mime_type?.includes('gif') ? '.gif'
|
: item.mime_type?.includes('gif') ? '.gif'
|
||||||
: item.mime_type?.includes('webp') ? '.webp'
|
: item.mime_type?.includes('webp') ? '.webp'
|
||||||
: '.jpg';
|
: '.jpg';
|
||||||
const localUri = `${CACHE_DIR}${safeId}${ext}`;
|
const destFile = new File(CACHE_DIR, `${safeId}${ext}`);
|
||||||
|
|
||||||
// Skip download if already cached
|
// Skip download if already cached and non-empty
|
||||||
const fileInfo = await FileSystem.getInfoAsync(localUri);
|
if (destFile.exists && destFile.size > 1024) {
|
||||||
if (fileInfo.exists && fileInfo.size > 0) {
|
results.push({ ...item, local_path: destFile.uri });
|
||||||
results.push({ ...item, local_path: localUri });
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download with auth header — append =d to get the full-res download
|
// The Picker API baseUrl is a direct download link when used with
|
||||||
const downloadUrl = `${item.picker_base_url}=d`;
|
// the Authorization header. No suffix needed.
|
||||||
const download = await FileSystem.downloadAsync(downloadUrl, localUri, {
|
console.log(`Downloading: ${item.title} (${item.picker_base_url.substring(0, 80)}…)`);
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (download.status === 200) {
|
const downloaded = await File.downloadFileAsync(
|
||||||
results.push({ ...item, local_path: download.uri });
|
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 {
|
} 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);
|
results.push(item);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -54,7 +54,12 @@ const PhotoGrid = ({
|
|||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
{item.local_path ? (
|
{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 }]}>
|
<View style={[styles.placeholder, { backgroundColor: theme.borderLight }]}>
|
||||||
<Text style={{ fontSize: 28 }}>📷</Text>
|
<Text style={{ fontSize: 28 }}>📷</Text>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import DatabaseService from '../services/DatabaseService';
|
|||||||
import DeviceMediaService from '../services/DeviceMediaService';
|
import DeviceMediaService from '../services/DeviceMediaService';
|
||||||
import GoogleAUTH from '../AUTH/GoogleAUTH';
|
import GoogleAUTH from '../AUTH/GoogleAUTH';
|
||||||
import NASAUTH from '../AUTH/NASAUTH';
|
import NASAUTH from '../AUTH/NASAUTH';
|
||||||
|
import { File } from 'expo-file-system';
|
||||||
import TopBar from '../components/TopBar';
|
import TopBar from '../components/TopBar';
|
||||||
import PhotoGrid from '../components/PhotoGrid';
|
import PhotoGrid from '../components/PhotoGrid';
|
||||||
import MediaViewer from '../components/MediaViewer';
|
import MediaViewer from '../components/MediaViewer';
|
||||||
@@ -132,7 +133,23 @@ const HomeScreen = ({ navigation, route }) => {
|
|||||||
// ─── Previously imported Google Photos (stored in DB) ───
|
// ─── Previously imported Google Photos (stored in DB) ───
|
||||||
try {
|
try {
|
||||||
const googleMedia = await DatabaseService.getMediaByService(userId, 'GOOGLE_PHOTOS');
|
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) {
|
} catch (err) {
|
||||||
console.error('DB Google media fetch error:', err);
|
console.error('DB Google media fetch error:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user