Google Drive Authentication
OAuth Flow
StreamVault uses OAuth 2.0 for secure Google Drive authentication.
// Start OAuth flow (opens browser)
export const gdrive_start_auth = async (): Promise<string>
// Complete authentication (waits for callback)
export const gdrive_complete_auth = async (): Promise<DriveAccountInfo>
// Disconnect from Google Drive
export const gdrive_disconnect = async (): Promise<{ message: string }>
DriveAccountInfo Interface
export interface DriveAccountInfo {
email: string;
display_name: string | null;
photo_url: string | null;
storage_used: number | null;
storage_limit: number | null;
}
Google account email address
User’s display name from Google profile
Total storage quota in bytes
OAuth Example
import {
gdrive_start_auth,
gdrive_complete_auth,
type DriveAccountInfo
} from '@/services/api';
async function connectGoogleDrive() {
try {
// Step 1: Open browser for authentication
const authUrl = await gdrive_start_auth();
console.log('Auth URL opened:', authUrl);
// Step 2: Wait for user to complete OAuth flow
// Backend receives callback and exchanges code for tokens
const accountInfo = await gdrive_complete_auth();
console.log(`Connected as: ${accountInfo.email}`);
console.log(`Display name: ${accountInfo.display_name}`);
if (accountInfo.storage_used && accountInfo.storage_limit) {
const usedGB = (accountInfo.storage_used / 1e9).toFixed(2);
const limitGB = (accountInfo.storage_limit / 1e9).toFixed(2);
console.log(`Storage: ${usedGB}GB / ${limitGB}GB`);
}
return accountInfo;
} catch (error) {
console.error('Failed to connect Google Drive:', error);
throw error;
}
}
Backend OAuth Implementation
// Start OAuth flow - returns auth URL
pub fn get_auth_url() -> String {
format!(
"https://streamvault-backend-server.onrender.com/auth/google?redirect_uri={}",
"http://localhost:18234/callback"
)
}
// Wait for OAuth callback (listens on localhost:18234)
pub async fn wait_for_oauth_callback() -> Result<GoogleTokens, String> {
let listener = TcpListener::bind("127.0.0.1:18234")
.map_err(|e| format!("Failed to bind: {}", e))?;
// Accept connection and parse tokens
// ...
}
Connection Status
Check if authenticated to Google Drive.
export const isGdriveConnected = async (): Promise<boolean>
export const getGdriveAccountInfo = async (): Promise<DriveAccountInfo | null>
Example Usage
import { isGdriveConnected, getGdriveAccountInfo } from '@/services/api';
const connected = await isGdriveConnected();
if (connected) {
const account = await getGdriveAccountInfo();
console.log(`Logged in as: ${account?.email}`);
} else {
console.log('Not connected to Google Drive');
// Show connect button
}
Folder Management
Track and scan Google Drive folders.
Cloud Folder Interface
interface CloudFolder {
id: string; // Google Drive folder ID
name: string; // Folder name
auto_scan: boolean; // Auto-scan on startup
}
Add Cloud Folder
export const addCloudFolder = async (
folderId: string,
folderName: string
): Promise<{ message: string }>
Display name for the folder
Remove Cloud Folder
export const removeCloudFolder = async (
folderId: string
): Promise<{ message: string }>
Google Drive folder ID to remove
Removing a cloud folder also deletes all indexed media from that folder in the library.
Get Cloud Folders
export const getCloudFolders = async (): Promise<CloudFolder[]>
Example Usage
import {
getCloudFolders,
addCloudFolder,
removeCloudFolder
} from '@/services/api';
// List tracked folders
const folders = await getCloudFolders();
folders.forEach(folder => {
console.log(`${folder.name} (${folder.id})`);
console.log(`Auto-scan: ${folder.auto_scan}`);
});
// Add new folder
await addCloudFolder('1abc...xyz', 'My Movies');
// Remove folder
await removeCloudFolder('1abc...xyz');
Browse Google Drive
List Folders
export interface DriveItem {
id: string;
name: string;
mime_type: string;
size?: string;
modified_time?: string;
parents?: string[];
web_content_link?: string;
}
export const gdrive_list_folders = async (
parentId?: string
): Promise<DriveItem[]>
Parent folder ID (omit for root folder)
List Files
export interface DriveListResponse {
files: DriveItem[];
next_page_token?: string;
}
export const gdrive_list_files = async (
folderId?: string
): Promise<DriveListResponse>
Example: Folder Browser
import { gdrive_list_folders, gdrive_list_files } from '@/services/api';
function FolderBrowser() {
const [currentFolder, setCurrentFolder] = useState<string | undefined>();
const [folders, setFolders] = useState<DriveItem[]>([]);
const [files, setFiles] = useState<DriveItem[]>([]);
useEffect(() => {
async function loadFolder() {
const [folderItems, fileItems] = await Promise.all([
gdrive_list_folders(currentFolder),
gdrive_list_files(currentFolder)
]);
setFolders(folderItems);
setFiles(fileItems.files);
}
loadFolder();
}, [currentFolder]);
return (
<div>
<h3>Folders</h3>
{folders.map(folder => (
<div key={folder.id} onClick={() => setCurrentFolder(folder.id)}>
📁 {folder.name}
</div>
))}
<h3>Files</h3>
{files.map(file => (
<div key={file.id}>
📄 {file.name}
</div>
))}
</div>
);
}
Scan Cloud Library
Index video files from Google Drive folders.
export interface CloudIndexResult {
success: boolean;
indexed_count: number;
skipped_count: number;
movies_count: number;
tv_count: number;
message: string;
}
export const gdrive_scan_folder = async (
folderId: string,
folderName: string
): Promise<CloudIndexResult>
export const scanAllCloudFolders = async (): Promise<CloudIndexResult>
Scan Behavior
- Auto-detection: Filenames are parsed to detect movies vs TV episodes
- TMDB Matching: Searches TMDB for metadata
- Duplicate Detection: Skips files already in library
- Episode Grouping: Groups episodes under parent TV show
Example Usage
import { gdrive_scan_folder, scanAllCloudFolders } from '@/services/api';
// Scan specific folder
const result = await gdrive_scan_folder('1abc...xyz', 'Movies');
console.log(result.message);
console.log(`Indexed: ${result.indexed_count}`);
console.log(`Movies: ${result.movies_count}`);
console.log(`TV Episodes: ${result.tv_count}`);
console.log(`Skipped (already indexed): ${result.skipped_count}`);
// Scan all tracked folders
const allResults = await scanAllCloudFolders();
console.log(`Total indexed: ${allResults.indexed_count}`);
Backend Implementation
#[tauri::command]
async fn gdrive_scan_folder(
state: State<'_, AppState>,
window: Window,
folder_id: String,
folder_name: String,
) -> Result<CloudIndexResult, String> {
// Get video files from folder
let files = state.gdrive_client.list_video_files(&folder_id, true).await?;
// Parse filenames and fetch TMDB metadata
// Insert into database with is_cloud=1
// ...
}
Cloud Streaming
Get streaming URLs for cloud files.
export const gdrive_get_stream_url = async (
fileId: string
): Promise<[string, string]>
Access token for authorization
Example Usage
import { gdrive_get_stream_url } from '@/services/api';
async function playCloudVideo(cloudFileId: string) {
const [streamUrl, accessToken] = await gdrive_get_stream_url(cloudFileId);
// Use with video player
videoPlayer.src = streamUrl;
// Include token in requests if needed
fetch(streamUrl, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
}
export const gdrive_get_file_metadata = async (
fileId: string
): Promise<DriveItem>
Example Usage
import { gdrive_get_file_metadata } from '@/services/api';
const metadata = await gdrive_get_file_metadata(fileId);
console.log(`Name: ${metadata.name}`);
console.log(`Size: ${metadata.size} bytes`);
console.log(`Modified: ${metadata.modified_time}`);
console.log(`MIME type: ${metadata.mime_type}`);
Video File Filtering
List only video files in a folder.
export const gdrive_list_video_files = async (
folderId: string,
recursive: boolean
): Promise<DriveItem[]>
Whether to scan subfolders
- MP4 (
video/mp4)
- MKV (
video/x-matroska)
- AVI (
video/avi)
- MOV (
video/quicktime)
- WebM (
video/webm)
- M4V (
video/x-m4v)
- WMV (
video/x-ms-wmv)
- FLV (
video/x-flv)
- TS (
video/mp2t)
Example Usage
import { gdrive_list_video_files } from '@/services/api';
// Get videos from folder only
const videos = await gdrive_list_video_files(folderId, false);
// Get videos from folder and all subfolders
const allVideos = await gdrive_list_video_files(folderId, true);
console.log(`Found ${allVideos.length} video files`);
Cloud Cache Management
Manage cached cloud files for offline playback.
export interface CloudCacheInfo {
enabled: boolean;
cache_dir: string | null;
total_size_bytes: number;
total_size_mb: number;
file_count: number;
max_size_mb: number;
expiry_hours: number;
}
export const getCloudCacheInfo = async (): Promise<CloudCacheInfo>
export const cleanupCloudCache = async (): Promise<{ message: string }>
export const clearCloudCache = async (): Promise<{ message: string }>
Example Usage
import {
getCloudCacheInfo,
cleanupCloudCache,
clearCloudCache
} from '@/services/api';
// Get cache statistics
const cache = await getCloudCacheInfo();
console.log(`Cache enabled: ${cache.enabled}`);
console.log(`Cache size: ${cache.total_size_mb} MB`);
console.log(`Files cached: ${cache.file_count}`);
console.log(`Max size: ${cache.max_size_mb} MB`);
// Clean expired cache files
const cleanupResult = await cleanupCloudCache();
console.log(cleanupResult.message);
// Clear all cache
const clearResult = await clearCloudCache();
console.log(clearResult.message);
Events
Listen for cloud scan events.
import { listen } from '@tauri-apps/api/event';
// Cloud scan started
await listen('cloud-indexing-started', (event) => {
const { count } = event.payload;
console.log(`Indexing ${count} files...`);
});
// Cloud scan completed
await listen('cloud-scan-complete', (event) => {
const { indexed, movies, tv, skipped } = event.payload;
console.log(`Scan complete: ${indexed} new items`);
console.log(`Movies: ${movies}, TV: ${tv}`);
console.log(`Skipped: ${skipped}`);
// Refresh library UI
refreshLibrary();
});
Backend Architecture
Token Management
pub struct GoogleDriveClient {
tokens: Arc<Mutex<Option<GoogleTokens>>>,
http_client: reqwest::Client,
}
impl GoogleDriveClient {
pub async fn get_access_token(&self) -> Result<String, String> {
// Check if token is expired
// Refresh if needed
// Return valid access token
}
}
API Requests
pub async fn list_files(
&self,
folder_id: Option<&str>,
page_token: Option<&str>,
) -> Result<DriveListResponse, String> {
let access_token = self.get_access_token().await?;
let url = format!(
"{}/files?q={}&fields=files(...),nextPageToken",
DRIVE_API_BASE,
query
);
let response = self.http_client
.get(&url)
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
response.json().await
}
Cloud cache management
getCloudCacheInfo()
Get information about the cloud cache including size and file count.
export const getCloudCacheInfo = async (): Promise<CloudCacheInfo>
Total cache size in bytes
Example:
const cacheInfo = await getCloudCacheInfo();
console.log(`Cache size: ${(cacheInfo.total_size_bytes / 1024 / 1024).toFixed(2)} MB`);
console.log(`Files cached: ${cacheInfo.file_count}`);
cleanupCloudCache()
Remove expired cache files based on expiry settings.
export const cleanupCloudCache = async (): Promise<{ message: string }>
Example:
const result = await cleanupCloudCache();
console.log(result.message); // "Cleaned up 5 expired files"
clearCloudCache()
Delete all files from the cloud cache.
export const clearCloudCache = async (): Promise<{ message: string }>
This will remove all cached cloud media files. Playback will require re-downloading.
Example:
const result = await clearCloudCache();
console.log(result.message); // "Cache cleared successfully"
Change detection
checkCloudChanges()
Check for changes in Google Drive since last scan.
async fn check_cloud_changes(state: State<'_, AppState>) -> Result<Vec<DriveChange>, String>
Returns list of files that have been added, modified, or deleted since the last scan.
Example:
import { invoke } from '@tauri-apps/api/tauri';
const changes = await invoke('check_cloud_changes');
console.log(`Found ${changes.length} changes`);