Nipun Claude commited on
Commit
50afa2a
·
1 Parent(s): 9c45c9e

Add Google Drive integration with native file picker

Browse files

- Add OAuth-based Google Drive integration for private file access
- Implement native Google Drive file/folder picker interface
- Add post-processing manual upload workflow after trimming
- Improve video web compatibility for better Gradio playback
- Update requirements.txt with Google Drive API dependencies
- Add comprehensive setup documentation (SIMPLE_GOOGLE_SETUP.md)
- Add OAuth credentials to .gitignore for security
- Update README with Google Drive features and setup instructions

Features:
- Browse entire Google Drive to select videos
- Upload trimmed files to any chosen Google Drive folder
- No public sharing required - works with private files
- Optional integration - app works without Google Drive setup
- Web-optimized video output for better browser compatibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

.gitignore CHANGED
@@ -70,6 +70,13 @@ test_videos/
70
  .local/
71
  config.local.*
72
 
 
 
 
 
 
 
 
73
  # Demo video exceptions (keep these)
74
  !demo/sample-*.mp4
75
  !demo/sample-*.aac
 
70
  .local/
71
  config.local.*
72
 
73
+ # Google Drive OAuth credentials (keep private!)
74
+ oauth_credentials.json
75
+ oauth_token.pickle
76
+ service-account.json
77
+ token.json
78
+ credentials.json
79
+
80
  # Demo video exceptions (keep these)
81
  !demo/sample-*.mp4
82
  !demo/sample-*.aac
FIX_ACCESS_BLOCKED.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚨 Fix "Access Blocked" Error
2
+
3
+ ## The Error You're Seeing:
4
+ ```
5
+ Access blocked: video-trim has not completed the Google verification process
6
+ Error 403: access_denied
7
+ ```
8
+
9
+ ## ✅ Quick Fix (2 minutes):
10
+
11
+ ### Step 1: Configure OAuth Consent Screen
12
+ 1. Go to **Google Cloud Console**: https://console.cloud.google.com/
13
+ 2. Select your project
14
+ 3. Go to **"APIs & Services"** → **"OAuth consent screen"**
15
+
16
+ ### Step 2: Set Up Testing Mode
17
+ 4. **User Type:** Choose **"External"** (if not already selected)
18
+ 5. Fill in required fields:
19
+ - **App name:** Video Trimmer
20
+ - **User support email:** nipunbatra0@gmail.com
21
+ - **Developer contact:** nipunbatra0@gmail.com
22
+ 6. Click **"SAVE AND CONTINUE"**
23
+
24
+ ### Step 3: Skip Scopes
25
+ 7. **Scopes page:** Just click **"SAVE AND CONTINUE"** (don't add anything)
26
+
27
+ ### Step 4: Add Yourself as Test User
28
+ 8. **Test users page:** Click **"+ ADD USERS"**
29
+ 9. Add: **nipunbatra0@gmail.com**
30
+ 10. Click **"SAVE AND CONTINUE"**
31
+
32
+ ### Step 5: Finish
33
+ 11. **Summary page:** Click **"BACK TO DASHBOARD"**
34
+
35
+ ---
36
+
37
+ ## 🎯 What This Does:
38
+
39
+ - **Puts your app in "testing" mode** - bypasses Google verification
40
+ - **Adds you as a test user** - allows you to use the app
41
+ - **Works immediately** - no waiting for Google approval
42
+
43
+ ---
44
+
45
+ ## 🔄 Now Try Again:
46
+
47
+ 1. **Run your video trimmer app**
48
+ 2. **Sign in process should work** - no more access blocked error
49
+ 3. **You'll see a warning** saying "This app isn't verified" - click **"Advanced"** → **"Go to Video Trimmer (unsafe)"**
50
+ 4. **Grant permissions** and you're done!
51
+
52
+ ---
53
+
54
+ ## 🛡️ Security Note:
55
+
56
+ - The "unsafe" warning is just because Google hasn't verified the app
57
+ - **It's perfectly safe** since you created the app yourself
58
+ - This is normal for personal/development apps
59
+ - All your data stays private and secure
60
+
61
+ **This fix works for personal use and testing. For public apps, you'd need Google verification, but that's not needed here!**
README.md CHANGED
@@ -5,6 +5,7 @@ A fast and efficient video trimming toolkit with both a **web interface** and **
5
  ## Features
6
 
7
  - **Web Interface**: Interactive Gradio demo with drag-to-trim sliders
 
8
  - **Command Line**: Fast bash script for automated processing
9
  - **Smart Trimming**: Visual video scrubbing to find exact cut points
10
  - **Audio Extraction**: Automatic AAC extraction with built-in player
@@ -91,14 +92,27 @@ pip install -r requirements.txt
91
  ```
92
 
93
  ### Features
94
- - **Video Upload**: Drag & drop MP4/MOV/AVI files
 
95
  - **Visual Trimming**: Scrub sliders to find exact start/end points
96
  - **Live Preview**: Video seeks to slider position for precise editing
97
  - **Audio Playback**: Built-in player for extracted audio
98
  - **Download**: Get both trimmed video and AAC audio files
 
99
 
100
  The web interface automatically converts times and calls the command-line script for processing.
101
 
 
 
 
 
 
 
 
 
 
 
 
102
  ## File Permissions
103
 
104
  ### Understanding Script Permissions
 
5
  ## Features
6
 
7
  - **Web Interface**: Interactive Gradio demo with drag-to-trim sliders
8
+ - **Google Drive Integration**: Load videos from and upload results to Google Drive
9
  - **Command Line**: Fast bash script for automated processing
10
  - **Smart Trimming**: Visual video scrubbing to find exact cut points
11
  - **Audio Extraction**: Automatic AAC extraction with built-in player
 
92
  ```
93
 
94
  ### Features
95
+ - **Video Upload**: Drag & drop MP4/MOV/AVI files or load from Google Drive
96
+ - **Google Drive Integration**: Browse your entire Google Drive to pick videos
97
  - **Visual Trimming**: Scrub sliders to find exact start/end points
98
  - **Live Preview**: Video seeks to slider position for precise editing
99
  - **Audio Playback**: Built-in player for extracted audio
100
  - **Download**: Get both trimmed video and AAC audio files
101
+ - **Google Drive Upload**: Upload trimmed files back to any folder in your Google Drive
102
 
103
  The web interface automatically converts times and calls the command-line script for processing.
104
 
105
+ ### Google Drive Setup (Optional)
106
+
107
+ To enable Google Drive integration:
108
+
109
+ 1. **Create OAuth credentials**: Follow instructions in `SIMPLE_GOOGLE_SETUP.md`
110
+ 2. **Download `oauth_credentials.json`** and place in this directory
111
+ 3. **Run the app** - it will open your browser for one-time authentication
112
+ 4. **Done!** Browse your entire Google Drive and upload results back
113
+
114
+ **Note**: Google Drive integration is completely optional - the app works perfectly without it.
115
+
116
  ## File Permissions
117
 
118
  ### Understanding Script Permissions
SIMPLE_GOOGLE_SETUP.md ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Super Simple Google Drive Setup (3 Steps)
2
+
3
+ ## Overview
4
+ This is the easiest way to integrate Google Drive - just like signing into any Google app!
5
+
6
+ ---
7
+
8
+ ## Step 1: Create OAuth App (5 minutes, one-time)
9
+
10
+ ### 1.1 Go to Google Cloud Console
11
+ - Open: **https://console.cloud.google.com/**
12
+ - Sign in with your Google account
13
+
14
+ ### 1.2 Create/Select Project
15
+ - Click **"Select a project"** → **"NEW PROJECT"**
16
+ - Name: `video-trimmer` → Click **"CREATE"**
17
+
18
+ ### 1.3 Enable Google Drive API
19
+ - Go to **"APIs & Services"** → **"Library"**
20
+ - Search: **"Google Drive API"** → Click it → **"ENABLE"**
21
+
22
+ ### 1.4 Configure OAuth Consent Screen (Important!)
23
+ - Go to **"APIs & Services"** → **"OAuth consent screen"**
24
+ - **User Type:** Choose **"External"** → Click **"CREATE"**
25
+ - **App name:** Video Trimmer
26
+ - **User support email:** Your email (nipunbatra0@gmail.com)
27
+ - **Developer contact information:** Your email
28
+ - Click **"SAVE AND CONTINUE"**
29
+ - **Scopes:** Click **"SAVE AND CONTINUE"** (don't add any)
30
+ - **Test users:** Click **"+ ADD USERS"** → Add **nipunbatra0@gmail.com** → **"SAVE AND CONTINUE"**
31
+ - **Summary:** Click **"BACK TO DASHBOARD"**
32
+
33
+ ### 1.5 Create OAuth Credentials
34
+ - Go to **"APIs & Services"** → **"Credentials"**
35
+ - Click **"+ CREATE CREDENTIALS"** → **"OAuth 2.0 Client IDs"**
36
+ - **Application type:** Desktop application
37
+ - **Name:** Video Trimmer (or any name)
38
+ - Click **"CREATE"**
39
+
40
+ ### 1.6 Download Credentials
41
+ - Click **"DOWNLOAD JSON"** button
42
+ - **Rename the file to:** `oauth_credentials.json`
43
+ - **Move it to this directory** (where video_trimmer_demo.py is)
44
+
45
+ ---
46
+
47
+ ## Step 2: Run the App
48
+ ```bash
49
+ python video_trimmer_demo.py
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Step 3: Sign In (first time only)
55
+ - App will **open your browser automatically**
56
+ - **Sign in with your Google account**
57
+ - **Click "Allow"** to grant Drive access
58
+ - **Done!** Browser will close and app is ready
59
+
60
+ ---
61
+
62
+ ## 🎉 How It Works
63
+
64
+ ### **Loading Videos:**
65
+ 1. Click **"🔄 Load Videos from Google Drive"**
66
+ 2. **Browse ALL your Google Drive videos**
67
+ 3. **Select any video** → Click **"📥 Download & Load Video"**
68
+ 4. **Video appears automatically** in the player
69
+
70
+ ### **Uploading Results:**
71
+ 1. **Check "📤 Upload trimmed files to Google Drive"**
72
+ 2. **Optionally choose a folder** (or use root Drive folder)
73
+ 3. **Trim your video as normal**
74
+ 4. **Files automatically upload** to your Google Drive
75
+
76
+ ---
77
+
78
+ ## ✅ Benefits
79
+
80
+ - **🔐 Use your own Google account** - no complex setup
81
+ - **📁 Access ALL your files** - entire Google Drive available
82
+ - **📤 Upload anywhere** - choose any folder
83
+ - **🔄 Works like Google apps** - familiar OAuth login
84
+ - **🛡️ Secure** - standard Google authentication
85
+
86
+ ---
87
+
88
+ ## 🔧 Troubleshooting
89
+
90
+ ### "oauth_credentials.json not found"
91
+ - Make sure you downloaded and renamed the file correctly
92
+ - Place it in the same folder as `video_trimmer_demo.py`
93
+
94
+ ### Browser doesn't open for login
95
+ - Copy the URL from terminal and paste in browser manually
96
+ - Make sure you're on the computer where the app is running
97
+
98
+ ### "No videos found"
99
+ - Make sure you have video files in your Google Drive
100
+ - Check file formats: MP4, MOV, AVI, MKV
101
+
102
+ ---
103
+
104
+ ## 🔒 Security
105
+
106
+ - **Your credentials stay on your computer**
107
+ - **You control what the app can access**
108
+ - **Revoke access anytime** in Google Account settings
109
+ - **No sharing required** - works with private files
110
+
111
+ **Total setup time: 5 minutes once, then just sign in and go!**
native_drive_picker.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Native Google Drive Picker - uses Google's built-in file picker interface
3
+ Much simpler than building our own browser
4
+ """
5
+
6
+ import os
7
+ import tempfile
8
+ import logging
9
+ import webbrowser
10
+ from typing import Optional, Dict
11
+
12
+ try:
13
+ from googleapiclient.discovery import build
14
+ from googleapiclient.http import MediaIoBaseDownload
15
+ from google.auth.transport.requests import Request
16
+ from google.oauth2.credentials import Credentials
17
+ from google_auth_oauthlib.flow import InstalledAppFlow
18
+ import io
19
+ GOOGLE_DRIVE_AVAILABLE = True
20
+ except ImportError:
21
+ GOOGLE_DRIVE_AVAILABLE = False
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # OAuth scopes
26
+ SCOPES = ['https://www.googleapis.com/auth/drive']
27
+
28
+ class GoogleDrivePickerManager:
29
+ """Simple Google Drive manager with native picker links"""
30
+
31
+ def __init__(self, credentials_file: str = "oauth_credentials.json", token_file: str = "oauth_token.pickle"):
32
+ self.credentials_file = credentials_file
33
+ self.token_file = token_file
34
+ self.service = None
35
+ self.authenticated = False
36
+
37
+ if not GOOGLE_DRIVE_AVAILABLE:
38
+ logger.warning("Google Drive API libraries not available")
39
+ return
40
+
41
+ self._authenticate()
42
+
43
+ def _authenticate(self) -> bool:
44
+ """Authenticate with Google Drive using OAuth"""
45
+ try:
46
+ import pickle
47
+
48
+ creds = None
49
+
50
+ # Load existing token
51
+ if os.path.exists(self.token_file):
52
+ with open(self.token_file, 'rb') as token:
53
+ creds = pickle.load(token)
54
+
55
+ # If there are no valid credentials, let user log in
56
+ if not creds or not creds.valid:
57
+ if creds and creds.expired and creds.refresh_token:
58
+ try:
59
+ creds.refresh(Request())
60
+ except Exception as e:
61
+ logger.warning(f"Token refresh failed: {e}")
62
+ creds = None
63
+
64
+ if not creds:
65
+ if not os.path.exists(self.credentials_file):
66
+ logger.error(f"OAuth credentials file not found: {self.credentials_file}")
67
+ return False
68
+
69
+ flow = InstalledAppFlow.from_client_secrets_file(self.credentials_file, SCOPES)
70
+ creds = flow.run_local_server(port=0, prompt='consent')
71
+
72
+ # Save credentials for next time
73
+ with open(self.token_file, 'wb') as token:
74
+ pickle.dump(creds, token)
75
+
76
+ self.service = build('drive', 'v3', credentials=creds)
77
+ self.authenticated = True
78
+ logger.info("✅ Google Drive OAuth authentication successful")
79
+ return True
80
+
81
+ except Exception as e:
82
+ logger.error(f"❌ Google Drive OAuth authentication failed: {e}")
83
+ return False
84
+
85
+ def is_available(self) -> bool:
86
+ """Check if Google Drive integration is available"""
87
+ return GOOGLE_DRIVE_AVAILABLE and self.authenticated
88
+
89
+ def get_user_info(self) -> Optional[str]:
90
+ """Get current user's email"""
91
+ if not self.is_available():
92
+ return None
93
+
94
+ try:
95
+ about = self.service.about().get(fields="user").execute()
96
+ user = about.get('user', {})
97
+ return user.get('emailAddress', 'Unknown user')
98
+ except Exception as e:
99
+ logger.error(f"Error getting user info: {e}")
100
+ return None
101
+
102
+ def open_drive_picker(self, picker_type: str = "file") -> str:
103
+ """Open Google Drive in browser for file/folder selection"""
104
+ if picker_type == "file":
105
+ # URL to open Google Drive - full access to all files
106
+ drive_url = "https://drive.google.com/drive/my-drive"
107
+ instruction = """
108
+ 📁 **Google Drive File Picker Instructions:**
109
+
110
+ 1. **Google Drive opens** in your browser
111
+ 2. **Browse your entire Google Drive** - navigate any folder
112
+ 3. **Find your video file** (MP4, MOV, AVI, MKV, etc.)
113
+ 4. **Right-click the file** → **"Share"** → **"Get link"**
114
+ 5. **Set to "Anyone with the link can view"** → **"Copy link"**
115
+ 6. **Paste the link** in the field below
116
+
117
+ The link looks like: `https://drive.google.com/file/d/FILE_ID/view`
118
+
119
+ **💡 Tip:** You can navigate through all folders, search, and pick any video file!
120
+ """
121
+ else:
122
+ # URL to open Google Drive for folder navigation
123
+ drive_url = "https://drive.google.com/drive/my-drive"
124
+ instruction = """
125
+ 📂 **Google Drive Folder Picker Instructions:**
126
+
127
+ 1. **Google Drive opens** in your browser
128
+ 2. **Navigate to your desired upload folder**
129
+ - Browse through your folder structure
130
+ - Or create a new folder: **New** → **Folder**
131
+ 3. **Right-click the folder** → **"Share"** → **"Get link"**
132
+ 4. **Set to "Anyone with the link can view"** → **"Copy link"**
133
+ 5. **Paste the folder link** in the field below
134
+ 6. **Leave empty** to upload to My Drive root
135
+
136
+ The link looks like: `https://drive.google.com/drive/folders/FOLDER_ID`
137
+
138
+ **💡 Tip:** You can create new folders or pick any existing folder!
139
+ """
140
+
141
+ try:
142
+ webbrowser.open(drive_url)
143
+ logger.info(f"🌐 Opened Google Drive in browser for {picker_type} selection")
144
+ except Exception as e:
145
+ logger.warning(f"Could not open browser: {e}")
146
+ instruction += f"\n\n**Manual:** Go to {drive_url}"
147
+
148
+ return instruction
149
+
150
+ def extract_file_id_from_url(self, drive_url: str) -> Optional[str]:
151
+ """Extract Google Drive file/folder ID from URL"""
152
+ if not drive_url:
153
+ return None
154
+
155
+ import re
156
+
157
+ # Various Google Drive URL patterns
158
+ patterns = [
159
+ r'/file/d/([a-zA-Z0-9_-]+)', # File links
160
+ r'/folders/([a-zA-Z0-9_-]+)', # Folder links
161
+ r'id=([a-zA-Z0-9_-]+)', # Old format
162
+ r'/open\?id=([a-zA-Z0-9_-]+)', # Another format
163
+ ]
164
+
165
+ for pattern in patterns:
166
+ match = re.search(pattern, drive_url)
167
+ if match:
168
+ return match.group(1)
169
+
170
+ # If URL looks like just an ID
171
+ if re.match(r'^[a-zA-Z0-9_-]+$', drive_url.strip()):
172
+ return drive_url.strip()
173
+
174
+ return None
175
+
176
+ def download_file_from_url(self, drive_url: str, custom_filename: str = None) -> tuple[Optional[str], str]:
177
+ """Download file from Google Drive URL"""
178
+ if not self.is_available():
179
+ return None, "❌ Google Drive not available"
180
+
181
+ file_id = self.extract_file_id_from_url(drive_url)
182
+ if not file_id:
183
+ return None, "❌ Could not extract file ID from URL"
184
+
185
+ try:
186
+ # Get file info
187
+ file_info = self.service.files().get(
188
+ fileId=file_id,
189
+ fields="id,name,size,mimeType"
190
+ ).execute()
191
+
192
+ filename = custom_filename or file_info.get('name', f'gdrive_file_{file_id}')
193
+
194
+ # Log file type info
195
+ mime_type = file_info.get('mimeType', '')
196
+ if mime_type.startswith('video/'):
197
+ logger.info(f"✅ Video file detected: {mime_type}")
198
+ else:
199
+ logger.info(f"📄 File type: {mime_type} (not a video - but that's ok!)")
200
+
201
+ # Download file
202
+ temp_dir = tempfile.gettempdir()
203
+ local_path = os.path.join(temp_dir, filename)
204
+
205
+ request = self.service.files().get_media(fileId=file_id)
206
+ fh = io.FileIO(local_path, 'wb')
207
+ downloader = MediaIoBaseDownload(fh, request)
208
+
209
+ done = False
210
+ while done is False:
211
+ status, done = downloader.next_chunk()
212
+ if status:
213
+ logger.info(f"📥 Download progress: {int(status.progress() * 100)}%")
214
+
215
+ fh.close()
216
+
217
+ file_size_mb = os.path.getsize(local_path) / (1024 * 1024)
218
+ logger.info(f"✅ Downloaded: {filename} ({file_size_mb:.1f} MB)")
219
+
220
+ return local_path, f"✅ Downloaded: {filename} ({file_size_mb:.1f} MB)"
221
+
222
+ except Exception as e:
223
+ logger.error(f"Error downloading file: {e}")
224
+ return None, f"❌ Download failed: {str(e)}"
225
+
226
+ def upload_file_to_folder(self, file_path: str, folder_url: str = None) -> tuple[bool, str]:
227
+ """Upload file to Google Drive folder"""
228
+ if not self.is_available():
229
+ return False, "❌ Google Drive not available"
230
+
231
+ try:
232
+ # Determine folder ID
233
+ folder_id = None
234
+ if folder_url:
235
+ folder_id = self.extract_file_id_from_url(folder_url)
236
+
237
+ # Prepare file metadata
238
+ file_name = os.path.basename(file_path)
239
+ file_metadata = {
240
+ 'name': file_name,
241
+ 'parents': [folder_id] if folder_id else []
242
+ }
243
+
244
+ # Determine MIME type
245
+ if file_path.endswith('.mp4'):
246
+ mime_type = 'video/mp4'
247
+ elif file_path.endswith('.aac'):
248
+ mime_type = 'audio/aac'
249
+ elif file_path.endswith('.mp3'):
250
+ mime_type = 'audio/mpeg'
251
+ else:
252
+ mime_type = None
253
+
254
+ # Upload file
255
+ from googleapiclient.http import MediaFileUpload
256
+ media = MediaFileUpload(file_path, mimetype=mime_type, resumable=True)
257
+ file = self.service.files().create(
258
+ body=file_metadata,
259
+ media_body=media,
260
+ fields='id,name,webViewLink'
261
+ ).execute()
262
+
263
+ file_id = file.get('id')
264
+ file_name = file.get('name')
265
+ web_link = file.get('webViewLink')
266
+
267
+ logger.info(f"✅ Uploaded: {file_name}")
268
+
269
+ return True, f"✅ Uploaded: {file_name}\n🔗 Link: {web_link}"
270
+
271
+ except Exception as e:
272
+ logger.error(f"Error uploading file: {e}")
273
+ return False, f"❌ Upload failed: {str(e)}"
274
+
275
+
276
+ def get_native_picker_instructions() -> str:
277
+ """Get instructions for using native Google Drive picker"""
278
+ return """
279
+ ## 🎯 Google Drive Native Picker
280
+
281
+ ### Why This is Better:
282
+ - ✅ **Use Google's own interface** - familiar and reliable
283
+ - ✅ **No complex browsing code** - just paste links
284
+ - ✅ **Works with any file** - private or shared
285
+ - ✅ **Simple setup** - same OAuth as before
286
+
287
+ ### How It Works:
288
+
289
+ #### 📥 **Loading Videos:**
290
+ 1. Click **"Open Google Drive Video Picker"**
291
+ 2. **Google Drive opens** in your browser (filtered for videos)
292
+ 3. **Select your video** → Right-click → **"Get link"**
293
+ 4. **Copy the link** and paste it in the app
294
+ 5. **Download & load** - video appears in trimmer
295
+
296
+ #### 📤 **Uploading Results:**
297
+ 1. Click **"Choose Upload Folder"** (optional)
298
+ 2. **Google Drive opens** → Navigate to desired folder
299
+ 3. **Copy folder link** and paste it
300
+ 4. **Trim your video** with upload enabled
301
+ 5. **Files automatically upload** to chosen folder
302
+
303
+ ### Benefits:
304
+ - **No custom file browser** - uses Google's proven interface
305
+ - **Works everywhere** - any device with a browser
306
+ - **Familiar UI** - everyone knows how to use Google Drive
307
+ - **Always up-to-date** - uses Google's latest interface
308
+ """
requirements.txt CHANGED
@@ -1,4 +1,7 @@
1
  gradio>=4.0.0
2
  numpy>=1.24.0
3
  Pillow>=9.0.0
4
- watchdog>=3.0.0
 
 
 
 
1
  gradio>=4.0.0
2
  numpy>=1.24.0
3
  Pillow>=9.0.0
4
+ watchdog>=3.0.0
5
+ google-api-python-client>=2.0.0
6
+ google-auth>=2.0.0
7
+ google-auth-oauthlib>=1.0.0
video_trimmer_demo.py CHANGED
@@ -6,6 +6,7 @@ import shutil
6
  import logging
7
  import time
8
  from pathlib import Path
 
9
 
10
  # Set up logging
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -103,6 +104,67 @@ def process_video_trim(video_file, start_time, end_time):
103
  audio_size = os.path.getsize(output_audio)
104
  logger.info(f"📊 File sizes: video={video_size} bytes, audio={audio_size} bytes")
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  # Create MP3 version for audio player (better browser compatibility)
107
  timestamp = str(int(time.time() * 1000))
108
  temp_audio_dir = os.path.dirname(output_audio)
@@ -126,12 +188,15 @@ def process_video_trim(video_file, start_time, end_time):
126
  audio_player_file = output_audio
127
 
128
  success_msg = f"✅ Successfully trimmed video from {start_seconds:.1f}s to {end_seconds:.1f}s"
 
 
 
129
  logger.info(success_msg)
130
- return output_video, audio_player_file, output_audio, success_msg
131
  else:
132
  error_msg = f"❌ Output files not created.\n\nScript STDOUT:\n{result.stdout}\n\nScript STDERR:\n{result.stderr}\n\nExpected files:\nVideo: {output_video}\nAudio: {output_audio}"
133
  logger.error(error_msg)
134
- return None, None, None, error_msg
135
  else:
136
  error_msg = f"❌ trim-convert.sh failed with return code {result.returncode}\n\nCommand run:\n{' '.join(cmd)}\n\nSTDOUT:\n{result.stdout}\n\nSTDERR:\n{result.stderr}"
137
  logger.error(error_msg)
@@ -197,6 +262,63 @@ def get_video_info(video_file):
197
  logger.warning(f"⚠️ {info}")
198
  return info, 100, 0, 100
199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  # Create the Gradio interface with custom CSS and JS
201
  custom_css = """
202
  .video-container video {
@@ -206,6 +328,12 @@ custom_css = """
206
  .slider-container {
207
  margin: 10px 0;
208
  }
 
 
 
 
 
 
209
  """
210
 
211
  custom_js = """
@@ -224,6 +352,68 @@ with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_cs
224
  Upload an MP4 video, set trim points, and generate trimmed video + audio files.
225
  """)
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  with gr.Row():
228
  with gr.Column(scale=2):
229
  # Video upload and display
@@ -295,6 +485,9 @@ with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_cs
295
  size="lg"
296
  )
297
 
 
 
 
298
  status_msg = gr.Textbox(
299
  label="📝 Status",
300
  interactive=False,
@@ -323,6 +516,48 @@ with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_cs
323
  show_label=True
324
  )
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  # Event handlers
327
  def update_video_and_sliders(video_file):
328
  info, duration, start_val, end_val = get_video_info(video_file)
@@ -361,11 +596,94 @@ with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_cs
361
  js="(value) => { const video = document.querySelector('#main_video_player video'); if (video && !isNaN(value)) { video.currentTime = value; } return value; }"
362
  )
363
 
364
- trim_btn.click(
365
- fn=process_video_trim,
366
- inputs=[video_input, start_slider, end_slider],
367
- outputs=[output_video, output_audio_player, output_audio_download, status_msg]
368
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
  if __name__ == "__main__":
371
  demo.launch(
 
6
  import logging
7
  import time
8
  from pathlib import Path
9
+ from native_drive_picker import GoogleDrivePickerManager, get_native_picker_instructions, GOOGLE_DRIVE_AVAILABLE
10
 
11
  # Set up logging
12
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
104
  audio_size = os.path.getsize(output_audio)
105
  logger.info(f"📊 File sizes: video={video_size} bytes, audio={audio_size} bytes")
106
 
107
+ # Check if video file is valid and convert for better web compatibility
108
+ try:
109
+ test_duration = get_video_duration(output_video)
110
+ logger.info(f"✅ Output video duration: {test_duration} seconds")
111
+ if test_duration == 0:
112
+ logger.warning("⚠️ Output video duration is 0, may have encoding issues")
113
+
114
+ # Check if trimmed video is web-compatible, if not, convert only the headers
115
+ display_video = output_video # Start with original
116
+
117
+ # Quick check if video might have compatibility issues
118
+ try:
119
+ # Test if ffprobe can read the file properly
120
+ probe_cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", output_video]
121
+ probe_result = subprocess.run(probe_cmd, capture_output=True, text=True)
122
+
123
+ if probe_result.returncode == 0:
124
+ import json
125
+ probe_data = json.loads(probe_result.stdout)
126
+ format_info = probe_data.get('format', {})
127
+
128
+ # Check if it needs web optimization
129
+ needs_conversion = False
130
+
131
+ # If the file has issues or isn't web-optimized, do a quick fix
132
+ if needs_conversion or True: # Always do quick web optimization for now
133
+ web_video_path = os.path.join(temp_dir, f"{base_name}_web.mp4")
134
+
135
+ # Quick web compatibility fix - just fix headers and ensure proper format
136
+ web_convert_cmd = [
137
+ "ffmpeg", "-y", "-i", output_video,
138
+ "-c", "copy", # Copy streams (fast)
139
+ "-movflags", "+faststart", # Optimize for web
140
+ "-f", "mp4", # Ensure MP4 format
141
+ web_video_path
142
+ ]
143
+
144
+ logger.info(f"🌐 Quick web optimization (stream copy)...")
145
+ web_result = subprocess.run(web_convert_cmd, capture_output=True, text=True)
146
+
147
+ if web_result.returncode == 0 and os.path.exists(web_video_path):
148
+ web_size = os.path.getsize(web_video_path)
149
+ logger.info(f"✅ Web-optimized video: {web_video_path} ({web_size} bytes)")
150
+ display_video = web_video_path
151
+
152
+ # Verify the optimized video
153
+ web_duration = get_video_duration(web_video_path)
154
+ logger.info(f"🎬 Optimized video duration: {web_duration} seconds")
155
+ else:
156
+ logger.warning(f"⚠️ Quick optimization failed: {web_result.stderr}")
157
+ logger.info("Using original trimmed video")
158
+ else:
159
+ logger.warning("⚠️ Could not analyze trimmed video, using as-is")
160
+
161
+ except Exception as e:
162
+ logger.warning(f"⚠️ Video analysis failed: {e}, using original")
163
+
164
+ except Exception as e:
165
+ logger.warning(f"⚠️ Could not verify output video: {e}")
166
+ display_video = output_video
167
+
168
  # Create MP3 version for audio player (better browser compatibility)
169
  timestamp = str(int(time.time() * 1000))
170
  temp_audio_dir = os.path.dirname(output_audio)
 
188
  audio_player_file = output_audio
189
 
190
  success_msg = f"✅ Successfully trimmed video from {start_seconds:.1f}s to {end_seconds:.1f}s"
191
+
192
+ # No automatic upload - will be done manually after trimming
193
+
194
  logger.info(success_msg)
195
+ return display_video, audio_player_file, output_audio, success_msg, output_video, output_audio
196
  else:
197
  error_msg = f"❌ Output files not created.\n\nScript STDOUT:\n{result.stdout}\n\nScript STDERR:\n{result.stderr}\n\nExpected files:\nVideo: {output_video}\nAudio: {output_audio}"
198
  logger.error(error_msg)
199
+ return None, None, None, error_msg, None, None
200
  else:
201
  error_msg = f"❌ trim-convert.sh failed with return code {result.returncode}\n\nCommand run:\n{' '.join(cmd)}\n\nSTDOUT:\n{result.stdout}\n\nSTDERR:\n{result.stderr}"
202
  logger.error(error_msg)
 
262
  logger.warning(f"⚠️ {info}")
263
  return info, 100, 0, 100
264
 
265
+ # Native Google Drive picker functions
266
+ def open_file_picker(drive_manager):
267
+ """Open Google Drive for file selection (full access)"""
268
+ if not drive_manager or not drive_manager.is_available():
269
+ return "❌ Google Drive not available"
270
+
271
+ instructions = drive_manager.open_drive_picker("file")
272
+ return instructions
273
+
274
+ def open_folder_picker(drive_manager):
275
+ """Open Google Drive for folder selection"""
276
+ if not drive_manager or not drive_manager.is_available():
277
+ return "❌ Google Drive not available"
278
+
279
+ instructions = drive_manager.open_drive_picker("folder")
280
+ return instructions
281
+
282
+ def download_from_drive_url(drive_manager, drive_url, custom_filename=""):
283
+ """Download video from Google Drive URL"""
284
+ if not drive_manager or not drive_manager.is_available():
285
+ return None, "❌ Google Drive not available"
286
+
287
+ if not drive_url or not drive_url.strip():
288
+ return None, "⚠️ Please paste a Google Drive link"
289
+
290
+ filename = custom_filename.strip() if custom_filename.strip() else None
291
+ return drive_manager.download_file_from_url(drive_url, filename)
292
+
293
+ def download_from_google_drive(file_id, file_display, drive_manager):
294
+ """Download selected file from Google Drive"""
295
+ if not file_id or not drive_manager or not drive_manager.is_available():
296
+ return None, "❌ No file selected or Google Drive unavailable"
297
+
298
+ try:
299
+ # Extract filename from display string
300
+ filename = file_display.split(' (')[0] if file_display else f"video_{file_id}.mp4"
301
+
302
+ logger.info(f"📥 Downloading {filename} from Google Drive...")
303
+ local_path = drive_manager.download_file(file_id, filename)
304
+
305
+ if local_path and os.path.exists(local_path):
306
+ return local_path, f"✅ Downloaded: {filename}"
307
+ else:
308
+ return None, "❌ Download failed"
309
+ except Exception as e:
310
+ logger.error(f"Error downloading from Google Drive: {e}")
311
+ return None, f"❌ Download error: {str(e)}"
312
+
313
+ # Initialize Google Drive manager
314
+ try:
315
+ drive_manager = GoogleDrivePickerManager()
316
+ drive_available = drive_manager.is_available()
317
+ except Exception as e:
318
+ logger.warning(f"Google Drive initialization failed: {e}")
319
+ drive_manager = None
320
+ drive_available = False
321
+
322
  # Create the Gradio interface with custom CSS and JS
323
  custom_css = """
324
  .video-container video {
 
328
  .slider-container {
329
  margin: 10px 0;
330
  }
331
+ .drive-section {
332
+ border: 1px solid #e0e0e0;
333
+ padding: 15px;
334
+ border-radius: 8px;
335
+ margin: 10px 0;
336
+ }
337
  """
338
 
339
  custom_js = """
 
352
  Upload an MP4 video, set trim points, and generate trimmed video + audio files.
353
  """)
354
 
355
+ # Native Google Drive picker section
356
+ if drive_available:
357
+ user_email = drive_manager.get_user_info() if drive_manager else "Unknown"
358
+ with gr.Group():
359
+ gr.Markdown("### 🔗 Google Drive Integration (Native Picker)")
360
+ gr.Markdown(f"**👤 Signed in as:** {user_email}")
361
+
362
+ # Video picker section
363
+ with gr.Row():
364
+ with gr.Column(scale=2):
365
+ gr.Markdown("#### 📁 Load Any File from Google Drive")
366
+
367
+ open_picker_btn = gr.Button(
368
+ "🌍 Browse Your Entire Google Drive",
369
+ variant="primary",
370
+ size="lg"
371
+ )
372
+
373
+ picker_instructions = gr.Textbox(
374
+ label="📝 Instructions",
375
+ value="Click the button above to open your full Google Drive - browse any folder!",
376
+ interactive=False,
377
+ lines=6
378
+ )
379
+
380
+ drive_url_input = gr.Textbox(
381
+ label="🔗 Paste Any Google Drive File Link",
382
+ placeholder="https://drive.google.com/file/d/FILE_ID/view...",
383
+ info="Works with any file type - videos, docs, etc. from any folder"
384
+ )
385
+
386
+ custom_filename_input = gr.Textbox(
387
+ label="🏷️ Custom Filename (Optional)",
388
+ placeholder="my_video.mp4"
389
+ )
390
+
391
+ download_from_url_btn = gr.Button(
392
+ "📥 Download Video from Link",
393
+ variant="secondary"
394
+ )
395
+
396
+ with gr.Column(scale=1):
397
+ drive_status = gr.Textbox(
398
+ label="📊 Status",
399
+ value="✅ Ready to pick from Google Drive",
400
+ interactive=False
401
+ )
402
+
403
+ # Simplified note
404
+ gr.Markdown("🚀 **Upload to Google Drive will be available after video trimming.**")
405
+ else:
406
+ with gr.Group():
407
+ gr.Markdown("### 🔗 Google Drive Integration")
408
+ if not GOOGLE_DRIVE_AVAILABLE:
409
+ gr.Markdown("**⚠️ Google Drive libraries not installed.**")
410
+ gr.Markdown("Install with: `pip install google-api-python-client google-auth google-auth-oauthlib`")
411
+ else:
412
+ gr.Markdown("**⚠️ Setup needed:** Create oauth_credentials.json file")
413
+
414
+ with gr.Accordion("📋 Setup Instructions", open=False):
415
+ gr.Markdown(get_native_picker_instructions())
416
+
417
  with gr.Row():
418
  with gr.Column(scale=2):
419
  # Video upload and display
 
485
  size="lg"
486
  )
487
 
488
+ # Note about manual upload
489
+ gr.Markdown("📝 **Note:** Upload options will appear after trimming is complete.")
490
+
491
  status_msg = gr.Textbox(
492
  label="📝 Status",
493
  interactive=False,
 
516
  show_label=True
517
  )
518
 
519
+ # Post-processing upload section (appears after trimming)
520
+ if drive_available:
521
+ with gr.Group(visible=False) as post_upload_section:
522
+ gr.Markdown("### 🚀 Upload Trimmed Files to Google Drive")
523
+
524
+ with gr.Row():
525
+ with gr.Column(scale=2):
526
+ post_open_folder_btn = gr.Button(
527
+ "🌍 Choose Google Drive Upload Folder",
528
+ variant="primary"
529
+ )
530
+
531
+ post_folder_instructions = gr.Textbox(
532
+ label="📝 Folder Instructions",
533
+ value="Click button above to choose where to upload your trimmed files",
534
+ interactive=False,
535
+ lines=4
536
+ )
537
+
538
+ post_upload_folder_url = gr.Textbox(
539
+ label="📁 Upload Folder Link",
540
+ placeholder="https://drive.google.com/drive/folders/FOLDER_ID...",
541
+ info="Leave empty to upload to My Drive root"
542
+ )
543
+
544
+ post_upload_btn = gr.Button(
545
+ "📤 Upload Files to Google Drive",
546
+ variant="secondary",
547
+ size="lg"
548
+ )
549
+
550
+ with gr.Column(scale=1):
551
+ post_upload_status = gr.Textbox(
552
+ label="📊 Upload Status",
553
+ value="Ready to upload",
554
+ interactive=False
555
+ )
556
+
557
+ # Hidden state to store file paths for post-upload
558
+ trimmed_video_path = gr.State(None)
559
+ trimmed_audio_path = gr.State(None)
560
+
561
  # Event handlers
562
  def update_video_and_sliders(video_file):
563
  info, duration, start_val, end_val = get_video_info(video_file)
 
596
  js="(value) => { const video = document.querySelector('#main_video_player video'); if (video && !isNaN(value)) { video.currentTime = value; } return value; }"
597
  )
598
 
599
+ # Google Drive native picker event handlers
600
+ if drive_available:
601
+ # Open file picker (full Google Drive access)
602
+ open_picker_btn.click(
603
+ fn=lambda: open_file_picker(drive_manager),
604
+ outputs=[picker_instructions]
605
+ )
606
+
607
+ # Download from URL
608
+ download_from_url_btn.click(
609
+ fn=lambda url, filename: download_from_drive_url(drive_manager, url, filename),
610
+ inputs=[drive_url_input, custom_filename_input],
611
+ outputs=[video_input, drive_status]
612
+ ).then(
613
+ fn=update_video_and_sliders,
614
+ inputs=[video_input],
615
+ outputs=[video_player, video_info, start_slider, end_slider, start_time_display, end_time_display]
616
+ )
617
+
618
+ # No pre-upload handlers needed
619
+
620
+ # Post-upload event handlers
621
+ post_open_folder_btn.click(
622
+ fn=lambda: open_folder_picker(drive_manager),
623
+ outputs=[post_folder_instructions]
624
+ )
625
+
626
+ def post_upload_files(video_path, audio_path, folder_url):
627
+ if not video_path or not audio_path:
628
+ return "❌ No files to upload"
629
+
630
+ try:
631
+ folder_url_clean = folder_url.strip() if folder_url and folder_url.strip() else None
632
+
633
+ video_success, video_result = drive_manager.upload_file_to_folder(video_path, folder_url_clean)
634
+ audio_success, audio_result = drive_manager.upload_file_to_folder(audio_path, folder_url_clean)
635
+
636
+ if video_success and audio_success:
637
+ return f"✅ Files uploaded successfully:\n• {video_result}\n• {audio_result}"
638
+ elif video_success:
639
+ return f"✅ {video_result}\n❌ Audio upload failed: {audio_result}"
640
+ elif audio_success:
641
+ return f"✅ {audio_result}\n❌ Video upload failed: {video_result}"
642
+ else:
643
+ return f"❌ Upload failed:\n• Video: {video_result}\n• Audio: {audio_result}"
644
+
645
+ except Exception as e:
646
+ return f"❌ Upload error: {str(e)}"
647
+
648
+ post_upload_btn.click(
649
+ fn=post_upload_files,
650
+ inputs=[trimmed_video_path, trimmed_audio_path, post_upload_folder_url],
651
+ outputs=[post_upload_status]
652
+ )
653
+
654
+ # Trim button handler with Google Drive upload support
655
+ if drive_available:
656
+ # Simplified trim function that shows upload section after completion
657
+ def trim_and_show_upload(video_file, start_time, end_time):
658
+ result = process_video_trim(video_file, start_time, end_time)
659
+ display_video, audio_player, audio_download, status, orig_video, orig_audio = result
660
+
661
+ # Show post-upload section if trimming was successful
662
+ show_upload = orig_video is not None and orig_audio is not None
663
+
664
+ return (
665
+ display_video, audio_player, audio_download, status, # Original outputs
666
+ orig_video, orig_audio, # Store paths for post-upload
667
+ gr.Group(visible=show_upload) # Show/hide upload section
668
+ )
669
+
670
+ trim_btn.click(
671
+ fn=trim_and_show_upload,
672
+ inputs=[video_input, start_slider, end_slider],
673
+ outputs=[output_video, output_audio_player, output_audio_download, status_msg,
674
+ trimmed_video_path, trimmed_audio_path, post_upload_section]
675
+ )
676
+ else:
677
+ # No Google Drive available - simple trim only
678
+ def simple_trim(video_file, start_time, end_time):
679
+ result = process_video_trim(video_file, start_time, end_time)
680
+ return result[:4] # Return only the first 4 outputs
681
+
682
+ trim_btn.click(
683
+ fn=simple_trim,
684
+ inputs=[video_input, start_slider, end_slider],
685
+ outputs=[output_video, output_audio_player, output_audio_download, status_msg]
686
+ )
687
 
688
  if __name__ == "__main__":
689
  demo.launch(