feat: enhance database service with centralized error logging and improve media handling across services

This commit is contained in:
bobbert committed 2026-02-25 00:14:00 +00:00
1 parent 04046702d3
commit ad0cc1a7f5
5 files changed
+384 -165

No files matched your search

+81 -5
View File
@@ -1,6 +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 { AuthServiceBase } from './AuthServiceBase';
import { logError } from '../services/ErrorHandler';
@@ -29,6 +30,8 @@ const SCOPES = [
const PICKER_API = 'https://photospicker.googleapis.com/v1';
const CACHE_DIR = `${FileSystem.cacheDirectory}google_photos/`;
const REDIRECT_URI = `${Application.applicationId}:/oauthredirect`;
class GoogleAUTH extends AuthServiceBase {
@@ -276,6 +279,10 @@ class GoogleAUTH extends AuthServiceBase {
/**
* 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.
*/
_normalisePickerItems(items) {
return (items || []).map((item) => {
@@ -286,14 +293,80 @@ class GoogleAUTH extends AuthServiceBase {
source_id: item.id || file.id,
title: file.filename || 'Untitled',
description: '',
local_path: file.baseUrl ? `${file.baseUrl}=w400-h400` : null,
// 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,
media_type: isVideo ? 'video' : 'photo',
mime_type: file.mimeType || null,
created_date: file.mediaFileMetadata?.creationTime || null,
};
});
}
/**
* Download Picker images to local cache so <Image> can render them.
*
* The Picker API's baseUrl requires an Authorization header. React
* Native's <Image> can't add custom headers reliably, so we download
* each photo to a cache file and set local_path to the file:// URI.
*
* @param {Array} items — normalised items from _normalisePickerItems
* @returns {Array} — same items with local_path populated
*/
async downloadPickerPhotos(items) {
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 });
}
const results = [];
for (const item of items) {
if (!item.picker_base_url) {
results.push(item);
continue;
}
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}`;
// Skip download if already cached
const fileInfo = await FileSystem.getInfoAsync(localUri);
if (fileInfo.exists && fileInfo.size > 0) {
results.push({ ...item, local_path: localUri });
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}` },
});
if (download.status === 200) {
results.push({ ...item, local_path: download.uri });
} else {
logError('Download Picker Photo', `HTTP ${download.status} for ${item.title}`);
results.push(item);
}
} catch (err) {
logError('Download Picker Photo', err);
results.push(item);
}
}
return results;
}
/**
* Full interactive flow: create session → open picker in browser →
* wait for user to select photos → fetch the selected items.
@@ -339,11 +412,13 @@ class GoogleAUTH extends AuthServiceBase {
};
}
// 4. Fetch selected items
// 4. Fetch selected items and download to local cache
const result = await this._fetchPickerMediaItems(session.id, 100, null);
const mediaItems = this._normalisePickerItems(result.mediaItems);
let mediaItems = this._normalisePickerItems(result.mediaItems);
console.log(`Picker returned ${mediaItems.length} selected items`);
console.log(`Picker returned ${mediaItems.length} selected items, downloading…`);
mediaItems = await this.downloadPickerPhotos(mediaItems);
console.log(`Downloaded ${mediaItems.filter((m) => m.local_path).length} photos to cache`);
return {
mediaItems,
@@ -366,7 +441,8 @@ class GoogleAUTH extends AuthServiceBase {
}
const result = await this._fetchPickerMediaItems(sessionId, pageSize, pageToken);
const mediaItems = this._normalisePickerItems(result.mediaItems);
let mediaItems = this._normalisePickerItems(result.mediaItems);
mediaItems = await this.downloadPickerPhotos(mediaItems);
return {
mediaItems,