lukawskikacper commited on
Commit
0ad6b1b
·
1 Parent(s): febf860

Fix global search

Browse files
app/main.py CHANGED
@@ -34,6 +34,7 @@ def https_url_for(context: dict, name: str, **path_params: Any) -> URL:
34
  url: URL = request.url_for(name, **path_params)
35
  return url.replace(scheme="https")
36
 
 
37
  templates.env.globals["https_url_for"] = https_url_for
38
 
39
 
@@ -54,16 +55,48 @@ async def video_page(request: Request, video_id: str):
54
  if video and video.title:
55
  title = video.title
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  return templates.TemplateResponse(
58
  "video.html",
59
- {"request": request, "title": title, "video_id": video_id},
 
 
 
 
 
60
  )
61
 
62
 
63
  @app.get("/watch")
64
  async def watch_redirect(request: Request, v: str):
65
  # Redirect YouTube-style URLs to our video page
66
- return RedirectResponse(url=f"/video/{v}")
 
 
 
 
67
 
68
 
69
  # Include API routers
 
34
  url: URL = request.url_for(name, **path_params)
35
  return url.replace(scheme="https")
36
 
37
+
38
  templates.env.globals["https_url_for"] = https_url_for
39
 
40
 
 
55
  if video and video.title:
56
  title = video.title
57
 
58
+ # Get the start time from query parameters if available
59
+ start_time = 0
60
+ if "t" in request.query_params:
61
+ try:
62
+ # Try to parse the t parameter as seconds
63
+ t_param = request.query_params.get("t")
64
+ # First try as a float (seconds)
65
+ start_time = int(float(t_param))
66
+ except (ValueError, TypeError):
67
+ # If that fails, try parsing as MM:SS or HH:MM:SS format
68
+ try:
69
+ if ":" in t_param:
70
+ parts = t_param.split(":")
71
+ if len(parts) == 2: # MM:SS
72
+ start_time = int(parts[0]) * 60 + int(parts[1])
73
+ elif len(parts) == 3: # HH:MM:SS
74
+ start_time = (
75
+ int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2])
76
+ )
77
+ except (ValueError, TypeError, IndexError):
78
+ # If all parsing fails, default to 0
79
+ start_time = 0
80
+
81
  return templates.TemplateResponse(
82
  "video.html",
83
+ {
84
+ "request": request,
85
+ "title": title,
86
+ "video_id": video_id,
87
+ "start_time": start_time,
88
+ },
89
  )
90
 
91
 
92
  @app.get("/watch")
93
  async def watch_redirect(request: Request, v: str):
94
  # Redirect YouTube-style URLs to our video page
95
+ # If there's a t parameter, include it in the redirect
96
+ if "t" in request.query_params:
97
+ return RedirectResponse(url=f"/video/{v}?t={request.query_params.get('t')}")
98
+ else:
99
+ return RedirectResponse(url=f"/video/{v}")
100
 
101
 
102
  # Include API routers
app/models/video.py CHANGED
@@ -21,7 +21,8 @@ class Video(BaseModel):
21
  channel: Optional[str] = Field(None, description="Channel name")
22
  processed: bool = Field(False, description="Whether the video has been processed")
23
  created_at: Optional[int] = Field(
24
- None, description="Unix timestamp (seconds since epoch) when the video was processed"
 
25
  )
26
 
27
 
 
21
  channel: Optional[str] = Field(None, description="Channel name")
22
  processed: bool = Field(False, description="Whether the video has been processed")
23
  created_at: Optional[int] = Field(
24
+ None,
25
+ description="Unix timestamp (seconds since epoch) when the video was processed",
26
  )
27
 
28
 
app/services/video_service.py CHANGED
@@ -12,7 +12,9 @@ from app.models.video import VideoSegment, Video, SearchResult
12
  from app.services.qdrant_service import qdrant_client
13
 
14
  # Initialize the sentence transformer model
15
- model = SentenceTransformer("sentence-transformers/static-retrieval-mrl-en-v1", cache_folder="/tmp")
 
 
16
 
17
  # Collection names
18
  COLLECTION_NAME = "video_segments"
 
12
  from app.services.qdrant_service import qdrant_client
13
 
14
  # Initialize the sentence transformer model
15
+ model = SentenceTransformer(
16
+ "sentence-transformers/static-retrieval-mrl-en-v1", cache_folder="/tmp"
17
+ )
18
 
19
  # Collection names
20
  COLLECTION_NAME = "video_segments"
app/static/css/style.css CHANGED
@@ -135,3 +135,51 @@
135
  color: var(--accent-content, #581c87);
136
  white-space: nowrap;
137
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  color: var(--accent-content, #581c87);
136
  white-space: nowrap;
137
  }
138
+
139
+ /* Truncate text lines for search results */
140
+ .truncate-3-lines {
141
+ display: -webkit-box;
142
+ -webkit-line-clamp: 3;
143
+ -webkit-box-orient: vertical;
144
+ overflow: hidden;
145
+ text-overflow: ellipsis;
146
+ }
147
+
148
+ /* Modal styles */
149
+ .modal-box {
150
+ max-width: 80vw;
151
+ width: 100%;
152
+ }
153
+
154
+ @media (min-width: 768px) {
155
+ .modal-box {
156
+ max-width: 700px;
157
+ }
158
+ }
159
+
160
+ @media (min-width: 1024px) {
161
+ .modal-box {
162
+ max-width: 900px;
163
+ }
164
+ }
165
+
166
+ /* Search results in modal */
167
+ #search-results-container {
168
+ scrollbar-width: thin;
169
+ scrollbar-color: var(--primary) var(--base-200);
170
+ }
171
+
172
+ #search-results-container::-webkit-scrollbar {
173
+ width: 8px;
174
+ }
175
+
176
+ #search-results-container::-webkit-scrollbar-track {
177
+ background: var(--base-200);
178
+ border-radius: 10px;
179
+ }
180
+
181
+ #search-results-container::-webkit-scrollbar-thumb {
182
+ background-color: var(--primary);
183
+ border-radius: 10px;
184
+ border: 2px solid var(--base-200);
185
+ }
app/static/js/index.js CHANGED
@@ -7,6 +7,14 @@ document.addEventListener('DOMContentLoaded', () => {
7
  const recentlyProcessedCard = document.getElementById('recently-processed');
8
  const videoListContainer = document.getElementById('video-list');
9
 
 
 
 
 
 
 
 
 
10
  // Example video buttons
11
  const exampleButtons = document.querySelectorAll('.example-video');
12
 
@@ -265,4 +273,200 @@ document.addEventListener('DOMContentLoaded', () => {
265
 
266
  // Display recent videos on page load
267
  displayRecentVideos();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  });
 
7
  const recentlyProcessedCard = document.getElementById('recently-processed');
8
  const videoListContainer = document.getElementById('video-list');
9
 
10
+ // Check for search parameter in URL
11
+ const urlParams = new URLSearchParams(window.location.search);
12
+ const searchQuery = urlParams.get('search');
13
+ if (searchQuery) {
14
+ // Display search results
15
+ displaySearchResults(searchQuery);
16
+ }
17
+
18
  // Example video buttons
19
  const exampleButtons = document.querySelectorAll('.example-video');
20
 
 
273
 
274
  // Display recent videos on page load
275
  displayRecentVideos();
276
+
277
+ // Function to display search results
278
+ function displaySearchResults(query) {
279
+ // Try to use the modal if available
280
+ const searchModal = document.getElementById('search-results-modal');
281
+ const searchResultsContainer = document.getElementById('search-results-container');
282
+
283
+ if (searchModal && searchResultsContainer) {
284
+ // Update modal title with search term
285
+ const modalTitle = searchModal.querySelector('h3');
286
+ if (modalTitle) {
287
+ modalTitle.textContent = `Search Results for "${query}"`;
288
+ }
289
+
290
+ // Show the modal
291
+ searchModal.showModal();
292
+
293
+ // Clear previous results and show loading state
294
+ searchResultsContainer.innerHTML = `
295
+ <div class="flex justify-center items-center p-4">
296
+ <span class="loading loading-spinner loading-md"></span>
297
+ <span class="ml-2">Searching...</span>
298
+ </div>
299
+ `;
300
+
301
+ // Fetch and display results in the modal
302
+ fetchAndDisplayResults(query, searchResultsContainer);
303
+ } else {
304
+ // Fallback to the old implementation if modal is not available
305
+ // Show a search results card with loading indicator
306
+ processStatus.innerHTML = `
307
+ <div class="card bg-base-100 shadow-xl mt-4">
308
+ <div class="card-body">
309
+ <h2 class="card-title">Search Results for "${query}"</h2>
310
+ <div class="mt-4">
311
+ <div class="flex justify-center items-center p-4">
312
+ <span class="loading loading-spinner loading-md"></span>
313
+ <span class="ml-2">Searching...</span>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ </div>
318
+ `;
319
+
320
+ // Fetch and display results in the processStatus element
321
+ fetchAndDisplayResults(query, null, processStatus);
322
+ }
323
+ }
324
+
325
+ // Helper function to fetch and display search results
326
+ function fetchAndDisplayResults(query, container, fallbackContainer) {
327
+ // Fetch search results from API
328
+ fetch(`/api/video/search?query=${encodeURIComponent(query)}&limit=10`)
329
+ .then(response => {
330
+ if (!response.ok) {
331
+ throw new Error('Failed to perform search');
332
+ }
333
+ return response.json();
334
+ })
335
+ .then(results => {
336
+ if (results && results.length > 0) {
337
+ // Group results by video
338
+ const videoGroups = {};
339
+ // First pass: collect all video IDs that need titles
340
+ const videoIds = [];
341
+ results.forEach(result => {
342
+ const videoId = result.segment.video_id;
343
+ if (!videoGroups[videoId]) {
344
+ videoGroups[videoId] = {
345
+ videoId: videoId,
346
+ title: `Video ${videoId}`, // Default title, will be updated
347
+ segments: []
348
+ };
349
+ // Add to list of IDs to fetch titles for
350
+ videoIds.push(videoId);
351
+ }
352
+ videoGroups[videoId].segments.push(result);
353
+ });
354
+
355
+ // If we have video IDs, fetch their proper titles
356
+ const videoPromises = videoIds.map(videoId => {
357
+ return fetch(`/api/video/info/${videoId}`)
358
+ .then(response => response.ok ? response.json() : null)
359
+ .then(videoInfo => {
360
+ if (videoInfo && videoInfo.title && videoGroups[videoId]) {
361
+ videoGroups[videoId].title = videoInfo.title;
362
+ }
363
+ })
364
+ .catch(error => console.error(`Error fetching video info for ${videoId}:`, error));
365
+ });
366
+
367
+ // After all video titles are fetched, continue with rendering
368
+ Promise.all(videoPromises).then(() => {
369
+ // Generate results HTML
370
+ const resultsHTML = Object.values(videoGroups).map(group => {
371
+ const segmentsHTML = group.segments.map(result => {
372
+ return `
373
+ <a href="/video/${group.videoId}?t=${Math.floor(result.segment.start)}"
374
+ class="block p-2 hover:bg-base-200 rounded-md transition-colors duration-150 mb-2">
375
+ <div class="flex items-start">
376
+ <div class="text-primary font-mono mr-2">${formatTime(result.segment.start)}</div>
377
+ <div class="flex-grow">
378
+ <p class="truncate-3-lines">${result.segment.text}</p>
379
+ <div class="text-xs opacity-70 mt-1">Score: ${(result.score * 100).toFixed(1)}%</div>
380
+ </div>
381
+ </div>
382
+ </a>
383
+ `;
384
+ }).join('');
385
+
386
+ return `
387
+ <div class="mb-6">
388
+ <div class="flex items-center mb-2">
389
+ <img src="https://img.youtube.com/vi/${group.videoId}/default.jpg" alt="Thumbnail"
390
+ class="w-12 h-12 rounded-md mr-2">
391
+ <h3 class="text-lg font-bold">
392
+ <a href="/video/${group.videoId}" class="link">${group.title}</a>
393
+ </h3>
394
+ </div>
395
+ <div class="pl-4 border-l-2 border-primary">
396
+ ${segmentsHTML}
397
+ </div>
398
+ </div>
399
+ `;
400
+ }).join('');
401
+
402
+ // Display results in the appropriate container
403
+ if (container) {
404
+ container.innerHTML = resultsHTML;
405
+ } else if (fallbackContainer) {
406
+ fallbackContainer.innerHTML = `
407
+ <div class="card bg-base-100 shadow-xl mt-4">
408
+ <div class="card-body">
409
+ <h2 class="card-title">Search Results for "${query}"</h2>
410
+ <div class="mt-4">
411
+ ${resultsHTML}
412
+ </div>
413
+ </div>
414
+ </div>
415
+ `;
416
+ }
417
+ });
418
+ } else {
419
+ // No results found - display immediately, no need to wait for titles
420
+ const noResultsHTML = `
421
+ <div class="alert alert-info">
422
+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
423
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
424
+ </svg>
425
+ <span>No results found. Try a different search term or process more videos.</span>
426
+ </div>
427
+ `;
428
+
429
+ if (container) {
430
+ container.innerHTML = noResultsHTML;
431
+ } else if (fallbackContainer) {
432
+ fallbackContainer.innerHTML = `
433
+ <div class="card bg-base-100 shadow-xl mt-4">
434
+ <div class="card-body">
435
+ <h2 class="card-title">Search Results for "${query}"</h2>
436
+ <div class="mt-4">
437
+ ${noResultsHTML}
438
+ </div>
439
+ </div>
440
+ </div>
441
+ `;
442
+ }
443
+ }
444
+ })
445
+ .catch(error => {
446
+ console.error('Search error:', error);
447
+ const errorHTML = `
448
+ <div class="alert alert-error">
449
+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
450
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
451
+ </svg>
452
+ <span>Error performing search: ${error.message}</span>
453
+ </div>
454
+ `;
455
+
456
+ if (container) {
457
+ container.innerHTML = errorHTML;
458
+ } else if (fallbackContainer) {
459
+ fallbackContainer.innerHTML = `
460
+ <div class="card bg-base-100 shadow-xl mt-4">
461
+ <div class="card-body">
462
+ <h2 class="card-title">Search Results for "${query}"</h2>
463
+ <div class="mt-4">
464
+ ${errorHTML}
465
+ </div>
466
+ </div>
467
+ </div>
468
+ `;
469
+ }
470
+ });
471
+ }
472
  });
app/static/js/main.js CHANGED
@@ -20,13 +20,35 @@ document.addEventListener('DOMContentLoaded', () => {
20
  if (savedTheme) {
21
  document.documentElement.setAttribute('data-theme', savedTheme);
22
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  });
24
 
25
- // Format seconds to MM:SS format
26
  function formatTime(seconds) {
27
- const minutes = Math.floor(seconds / 60);
 
28
  const secs = Math.floor(seconds % 60);
29
- return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
 
 
 
 
 
30
  }
31
 
32
  // Error handling function
@@ -99,6 +121,138 @@ function extractVideoId(url) {
99
  return (match && match[7].length === 11) ? match[7] : null;
100
  }
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  // Load recent videos into the footer from the API
103
  function loadFooterRecentVideos() {
104
  const footerRecentVideos = document.getElementById('footer-recent-videos');
 
20
  if (savedTheme) {
21
  document.documentElement.setAttribute('data-theme', savedTheme);
22
  }
23
+
24
+ // Handle global search
25
+ const searchButton = document.getElementById('global-search-button');
26
+ const searchInput = document.getElementById('global-search');
27
+
28
+ if (searchButton && searchInput) {
29
+ searchButton.addEventListener('click', () => {
30
+ handleGlobalSearch();
31
+ });
32
+
33
+ searchInput.addEventListener('keypress', (e) => {
34
+ if (e.key === 'Enter') {
35
+ handleGlobalSearch();
36
+ }
37
+ });
38
+ }
39
  });
40
 
41
+ // Format seconds to HH:MM:SS format
42
  function formatTime(seconds) {
43
+ const hours = Math.floor(seconds / 3600);
44
+ const mins = Math.floor((seconds % 3600) / 60);
45
  const secs = Math.floor(seconds % 60);
46
+
47
+ if (hours > 0) {
48
+ return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
49
+ } else {
50
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
51
+ }
52
  }
53
 
54
  // Error handling function
 
121
  return (match && match[7].length === 11) ? match[7] : null;
122
  }
123
 
124
+ // Handle global search
125
+ function handleGlobalSearch() {
126
+ const searchInput = document.getElementById('global-search');
127
+ const searchTerm = searchInput.value.trim();
128
+
129
+ if (searchTerm) {
130
+ // Get modal elements
131
+ const searchModal = document.getElementById('search-results-modal');
132
+ const searchResultsContainer = document.getElementById('search-results-container');
133
+ const modalTitle = searchModal.querySelector('h3');
134
+
135
+ // Update modal title with search term
136
+ modalTitle.textContent = `Search Results for "${searchTerm}"`;
137
+
138
+ // Show the modal
139
+ searchModal.showModal();
140
+
141
+ // Clear previous results and show loading state
142
+ searchResultsContainer.innerHTML = `
143
+ <div class="flex justify-center items-center p-4">
144
+ <span class="loading loading-spinner loading-md"></span>
145
+ <span class="ml-2">Searching...</span>
146
+ </div>
147
+ `;
148
+
149
+ // Fetch search results from API
150
+ fetch(`/api/video/search?query=${encodeURIComponent(searchTerm)}&limit=10`)
151
+ .then(response => {
152
+ if (!response.ok) {
153
+ throw new Error('Failed to perform search');
154
+ }
155
+ return response.json();
156
+ })
157
+ .then(results => {
158
+ if (results && results.length > 0) {
159
+ // Group results by video
160
+ const videoGroups = {};
161
+ // First pass: collect all video IDs that need titles
162
+ const videoIds = [];
163
+ results.forEach(result => {
164
+ const videoId = result.segment.video_id;
165
+ if (!videoGroups[videoId]) {
166
+ videoGroups[videoId] = {
167
+ videoId: videoId,
168
+ title: `Video ${videoId}`, // Default title, will be updated
169
+ segments: []
170
+ };
171
+ // Add to list of IDs to fetch titles for
172
+ videoIds.push(videoId);
173
+ }
174
+ videoGroups[videoId].segments.push(result);
175
+ });
176
+
177
+ // If we have video IDs, fetch their proper titles
178
+ const videoPromises = videoIds.map(videoId => {
179
+ return fetch(`/api/video/info/${videoId}`)
180
+ .then(response => response.ok ? response.json() : null)
181
+ .then(videoInfo => {
182
+ if (videoInfo && videoInfo.title && videoGroups[videoId]) {
183
+ videoGroups[videoId].title = videoInfo.title;
184
+ }
185
+ })
186
+ .catch(error => console.error(`Error fetching video info for ${videoId}:`, error));
187
+ });
188
+
189
+ // After all video titles are fetched, continue with rendering
190
+ Promise.all(videoPromises).then(() => {
191
+ // Generate results HTML
192
+ const resultsHTML = Object.values(videoGroups).map(group => {
193
+ const segmentsHTML = group.segments.map(result => {
194
+ return `
195
+ <a href="/video/${group.videoId}?t=${Math.floor(result.segment.start)}"
196
+ class="block p-2 hover:bg-base-200 rounded-md transition-colors duration-150 mb-2">
197
+ <div class="flex items-start">
198
+ <div class="text-primary font-mono mr-2">${formatTime(result.segment.start)}</div>
199
+ <div class="flex-grow">
200
+ <p class="truncate-3-lines">${result.segment.text}</p>
201
+ <div class="text-xs opacity-70 mt-1">Score: ${(result.score * 100).toFixed(1)}%</div>
202
+ </div>
203
+ </div>
204
+ </a>
205
+ `;
206
+ }).join('');
207
+
208
+ return `
209
+ <div class="mb-6">
210
+ <div class="flex items-center mb-2">
211
+ <img src="https://img.youtube.com/vi/${group.videoId}/default.jpg" alt="Thumbnail"
212
+ class="w-12 h-12 rounded-md mr-2">
213
+ <h3 class="text-lg font-bold">
214
+ <a href="/video/${group.videoId}" class="link">${group.title}</a>
215
+ </h3>
216
+ </div>
217
+ <div class="pl-4 border-l-2 border-primary">
218
+ ${segmentsHTML}
219
+ </div>
220
+ </div>
221
+ `;
222
+ }).join('');
223
+
224
+ // Update search results container
225
+ searchResultsContainer.innerHTML = resultsHTML;
226
+ });
227
+ } else {
228
+ // No results found - display immediately, no need to wait for titles
229
+ searchResultsContainer.innerHTML = `
230
+ <div class="alert alert-info">
231
+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
232
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
233
+ </svg>
234
+ <span>No results found. Try a different search term or process more videos.</span>
235
+ </div>
236
+ `;
237
+ }
238
+ })
239
+ .catch(error => {
240
+ console.error('Search error:', error);
241
+ searchResultsContainer.innerHTML = `
242
+ <div class="alert alert-error">
243
+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
244
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
245
+ </svg>
246
+ <span>Error performing search: ${error.message}</span>
247
+ </div>
248
+ `;
249
+ });
250
+ } else {
251
+ // Show notification if search is empty
252
+ showToast('Please enter a search term', 'warning');
253
+ }
254
+ }
255
+
256
  // Load recent videos into the footer from the API
257
  function loadFooterRecentVideos() {
258
  const footerRecentVideos = document.getElementById('footer-recent-videos');
app/static/js/video.js CHANGED
@@ -11,10 +11,11 @@ document.addEventListener('DOMContentLoaded', () => {
11
  let ytPlayer = null;
12
  let isProcessingUrl = false;
13
 
14
- // Check if there's a search query in the URL
15
  const urlParams = new URLSearchParams(window.location.search);
16
  const searchQuery = urlParams.get('q');
17
  const processingUrl = urlParams.get('processing');
 
18
 
19
  // Format time to display as HH:MM:SS
20
  function formatTime(seconds) {
@@ -67,6 +68,7 @@ document.addEventListener('DOMContentLoaded', () => {
67
  // When player is ready
68
  function onPlayerReady(event) {
69
  console.log('Player ready');
 
70
  }
71
 
72
  // Load transcript segments
@@ -107,6 +109,17 @@ document.addEventListener('DOMContentLoaded', () => {
107
  `;
108
  } else {
109
  displayTranscript(segments);
 
 
 
 
 
 
 
 
 
 
 
110
  }
111
  })
112
  .catch(error => {
@@ -162,6 +175,11 @@ document.addEventListener('DOMContentLoaded', () => {
162
  // Ensure seconds is a number
163
  seconds = parseFloat(seconds);
164
 
 
 
 
 
 
165
  // Seek to time
166
  ytPlayer.seekTo(seconds, true);
167
 
@@ -173,12 +191,23 @@ document.addEventListener('DOMContentLoaded', () => {
173
  }
174
 
175
  // Highlight the current segment
176
- highlightSegment(seconds);
 
 
177
  } catch (error) {
178
  console.error('Error seeking to time:', error);
179
  }
180
  } else {
181
  console.error('YouTube player is not ready yet or seekTo method is not available');
 
 
 
 
 
 
 
 
 
182
  }
183
  }
184
 
@@ -189,6 +218,14 @@ document.addEventListener('DOMContentLoaded', () => {
189
  segment.classList.remove('highlight');
190
  });
191
 
 
 
 
 
 
 
 
 
192
  // Find the segment containing current time
193
  // Need to find by approximate match since floating point exact matches may not work
194
  const segments = document.querySelectorAll('.transcript-segment');
@@ -204,16 +241,32 @@ document.addEventListener('DOMContentLoaded', () => {
204
  }
205
  }
206
 
207
- // If exact time match not found, find the closest segment
208
- if (!currentSegment) {
 
209
  const exactMatch = document.querySelector(`.transcript-segment[data-start="${time}"]`);
210
  if (exactMatch) {
211
  currentSegment = exactMatch;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  }
213
  }
214
 
215
  if (currentSegment) {
216
  currentSegment.classList.add('highlight');
 
217
  currentSegment.scrollIntoView({ behavior: 'smooth', block: 'center' });
218
  }
219
  }
@@ -437,4 +490,105 @@ document.addEventListener('DOMContentLoaded', () => {
437
  // Set timeout to stop checking after 10 seconds
438
  setTimeout(() => clearInterval(checkTranscriptInterval), 10000);
439
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  });
 
11
  let ytPlayer = null;
12
  let isProcessingUrl = false;
13
 
14
+ // Check if there's a search query or timestamp in the URL
15
  const urlParams = new URLSearchParams(window.location.search);
16
  const searchQuery = urlParams.get('q');
17
  const processingUrl = urlParams.get('processing');
18
+ const startTime = urlParams.get('t');
19
 
20
  // Format time to display as HH:MM:SS
21
  function formatTime(seconds) {
 
68
  // When player is ready
69
  function onPlayerReady(event) {
70
  console.log('Player ready');
71
+ // Seeking will be handled by the dedicated timestamp handler code at the bottom
72
  }
73
 
74
  // Load transcript segments
 
109
  `;
110
  } else {
111
  displayTranscript(segments);
112
+
113
+ // If we have a timestamp in the URL, highlight the relevant segment
114
+ if (startTime) {
115
+ const timeInSeconds = parseFloat(startTime);
116
+ if (!isNaN(timeInSeconds)) {
117
+ // Find segment containing this time
118
+ setTimeout(() => {
119
+ highlightSegment(timeInSeconds);
120
+ }, 500); // Short delay to ensure segments are rendered
121
+ }
122
+ }
123
  }
124
  })
125
  .catch(error => {
 
175
  // Ensure seconds is a number
176
  seconds = parseFloat(seconds);
177
 
178
+ if (isNaN(seconds)) {
179
+ console.error('Invalid seconds value:', seconds);
180
+ return;
181
+ }
182
+
183
  // Seek to time
184
  ytPlayer.seekTo(seconds, true);
185
 
 
191
  }
192
 
193
  // Highlight the current segment
194
+ setTimeout(() => {
195
+ highlightSegment(seconds);
196
+ }, 300); // Short delay to ensure seek completes first
197
  } catch (error) {
198
  console.error('Error seeking to time:', error);
199
  }
200
  } else {
201
  console.error('YouTube player is not ready yet or seekTo method is not available');
202
+
203
+ // Queue the seek operation for when the player becomes available
204
+ console.log('Queueing seek operation for later...');
205
+ setTimeout(() => {
206
+ if (ytPlayer && typeof ytPlayer.seekTo === 'function') {
207
+ console.log('Player now ready, executing queued seek');
208
+ seekToTime(seconds);
209
+ }
210
+ }, 1000); // Try again in 1 second
211
  }
212
  }
213
 
 
218
  segment.classList.remove('highlight');
219
  });
220
 
221
+ // Wait until segments are available in the DOM
222
+ if (document.querySelectorAll('.transcript-segment').length === 0) {
223
+ console.log('No transcript segments found, waiting...');
224
+ // Retry after a short delay to allow transcript to load
225
+ setTimeout(() => highlightSegment(time), 500);
226
+ return;
227
+ }
228
+
229
  // Find the segment containing current time
230
  // Need to find by approximate match since floating point exact matches may not work
231
  const segments = document.querySelectorAll('.transcript-segment');
 
241
  }
242
  }
243
 
244
+ // If exact time match not found, find the closest segment by time
245
+ if (!currentSegment && segments.length > 0) {
246
+ // First try exact match
247
  const exactMatch = document.querySelector(`.transcript-segment[data-start="${time}"]`);
248
  if (exactMatch) {
249
  currentSegment = exactMatch;
250
+ } else {
251
+ // Find closest segment
252
+ let closestSegment = segments[0];
253
+ let closestDistance = Math.abs(parseFloat(closestSegment.dataset.start) - time);
254
+
255
+ segments.forEach(segment => {
256
+ const distance = Math.abs(parseFloat(segment.dataset.start) - time);
257
+ if (distance < closestDistance) {
258
+ closestDistance = distance;
259
+ closestSegment = segment;
260
+ }
261
+ });
262
+
263
+ currentSegment = closestSegment;
264
  }
265
  }
266
 
267
  if (currentSegment) {
268
  currentSegment.classList.add('highlight');
269
+ // Ensure the segment is visible in the transcript container
270
  currentSegment.scrollIntoView({ behavior: 'smooth', block: 'center' });
271
  }
272
  }
 
490
  // Set timeout to stop checking after 10 seconds
491
  setTimeout(() => clearInterval(checkTranscriptInterval), 10000);
492
  }
493
+
494
+ // If there's a timestamp in the URL, ensure it will be seeked to after transcript loads
495
+ if (startTime) { // Handle timestamp regardless of search query
496
+ let timeInSeconds = parseFloat(startTime);
497
+
498
+ // If parsing fails, try to parse as HH:MM:SS format
499
+ if (isNaN(timeInSeconds) && typeof startTime === 'string') {
500
+ // Try to parse HH:MM:SS or MM:SS format
501
+ const timeParts = startTime.split(':').map(part => parseInt(part, 10));
502
+ if (timeParts.length === 3) {
503
+ // HH:MM:SS format
504
+ timeInSeconds = timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2];
505
+ } else if (timeParts.length === 2) {
506
+ // MM:SS format
507
+ timeInSeconds = timeParts[0] * 60 + timeParts[1];
508
+ }
509
+ }
510
+
511
+ if (!isNaN(timeInSeconds)) {
512
+ console.log('Will seek to timestamp:', timeInSeconds, 'seconds');
513
+
514
+ // Try immediately if player is ready
515
+ if (ytPlayer && typeof ytPlayer.seekTo === 'function') {
516
+ console.log('YouTube player is ready, seeking now');
517
+ seekToTime(timeInSeconds);
518
+ // Also try to play the video as a fallback if autoplay doesn't work
519
+ try {
520
+ ytPlayer.playVideo();
521
+ // Unmute the video after a short delay
522
+ setTimeout(() => {
523
+ try {
524
+ ytPlayer.unMute();
525
+ // Set volume to a reasonable level
526
+ ytPlayer.setVolume(80);
527
+ } catch (e) {
528
+ console.warn('Could not unmute video:', e);
529
+ }
530
+ }, 1000);
531
+ } catch (e) {
532
+ console.warn('Could not autoplay video:', e);
533
+ }
534
+ }
535
+
536
+ // Also set up a backup interval to ensure we seek once everything is ready
537
+ const checkReadyInterval = setInterval(() => {
538
+ if (ytPlayer && typeof ytPlayer.seekTo === 'function') {
539
+ if (transcriptSegments.length > 0) {
540
+ clearInterval(checkReadyInterval);
541
+ console.log('Everything loaded, seeking to timestamp:', timeInSeconds);
542
+ seekToTime(timeInSeconds);
543
+ // Also try to play the video
544
+ try {
545
+ ytPlayer.playVideo();
546
+ // Unmute the video after a short delay
547
+ setTimeout(() => {
548
+ try {
549
+ ytPlayer.unMute();
550
+ ytPlayer.setVolume(80);
551
+ } catch (e) {
552
+ console.warn('Could not unmute video after delay:', e);
553
+ }
554
+ }, 1000);
555
+ } catch (e) {
556
+ console.warn('Could not autoplay video after delay:', e);
557
+ }
558
+ } else {
559
+ console.log('Waiting for transcript segments to load...');
560
+ }
561
+ } else {
562
+ console.log('Waiting for YouTube player to be ready...');
563
+ }
564
+ }, 500);
565
+
566
+ // Set timeout to stop checking after 10 seconds
567
+ setTimeout(() => {
568
+ clearInterval(checkReadyInterval);
569
+ // Final attempt
570
+ if (ytPlayer && typeof ytPlayer.seekTo === 'function') {
571
+ console.log('Final attempt to seek to:', timeInSeconds);
572
+ seekToTime(timeInSeconds);
573
+ // One final attempt to play
574
+ try {
575
+ ytPlayer.playVideo();
576
+ // Unmute the video after a short delay
577
+ setTimeout(() => {
578
+ try {
579
+ ytPlayer.unMute();
580
+ ytPlayer.setVolume(80);
581
+ } catch (e) {
582
+ console.warn('Could not unmute video on final attempt:', e);
583
+ }
584
+ }, 1000);
585
+ } catch (e) {
586
+ console.warn('Could not autoplay video on final attempt:', e);
587
+ }
588
+ }
589
+ }, 10000);
590
+ } else {
591
+ console.warn('Could not parse timestamp from URL:', startTime);
592
+ }
593
+ }
594
  });
app/templates/base.html CHANGED
@@ -75,6 +75,26 @@
75
  </div>
76
  </footer>
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  <!-- Scripts -->
79
  <script src="{{ https_url_for('static', path='/js/main.js') }}"></script>
80
  {% block scripts %}{% endblock %}
 
75
  </div>
76
  </footer>
77
 
78
+ <!-- Search Results Modal -->
79
+ <dialog id="search-results-modal" class="modal">
80
+ <div class="modal-box max-w-4xl">
81
+ <form method="dialog">
82
+ <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
83
+ </form>
84
+ <h3 class="font-bold text-lg mb-4">Search Results</h3>
85
+ <div id="search-results-container" class="overflow-y-auto max-h-[70vh]">
86
+ <!-- Search results will be loaded here by JavaScript -->
87
+ <div class="flex justify-center items-center p-4">
88
+ <span class="loading loading-spinner loading-md"></span>
89
+ <span class="ml-2">Searching...</span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ <form method="dialog" class="modal-backdrop">
94
+ <button>close</button>
95
+ </form>
96
+ </dialog>
97
+
98
  <!-- Scripts -->
99
  <script src="{{ https_url_for('static', path='/js/main.js') }}"></script>
100
  {% block scripts %}{% endblock %}
app/templates/video.html CHANGED
@@ -7,7 +7,7 @@
7
  <div class="card-body p-4">
8
  <div class="aspect-video">
9
  <iframe id="youtube-player" class="w-full h-full"
10
- src="https://www.youtube.com/embed/{{ video_id }}?enablejsapi=1"
11
  frameborder="0"
12
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
13
  allowfullscreen>
 
7
  <div class="card-body p-4">
8
  <div class="aspect-video">
9
  <iframe id="youtube-player" class="w-full h-full"
10
+ src="https://www.youtube.com/embed/{{ video_id }}?enablejsapi=1&start={{ start_time }}{% if start_time > 0 %}&autoplay=1&mute=1{% endif %}"
11
  frameborder="0"
12
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
13
  allowfullscreen>
gunicorn.conf.py CHANGED
@@ -17,7 +17,7 @@ bind = "0.0.0.0:7860"
17
 
18
  # Logging
19
  accesslog = "-" # Log to stdout
20
- errorlog = "-" # Log to stderr
21
  loglevel = "info"
22
 
23
  # Timeout configuration
@@ -29,4 +29,4 @@ worker_connections = 1000 # Maximum number of connections each worker can handl
29
  keepalive = 5 # Seconds to wait between client requests before closing connection
30
 
31
  # For better performance with Uvicorn
32
- proc_name = "vibe-coding-rag"
 
17
 
18
  # Logging
19
  accesslog = "-" # Log to stdout
20
+ errorlog = "-" # Log to stderr
21
  loglevel = "info"
22
 
23
  # Timeout configuration
 
29
  keepalive = 5 # Seconds to wait between client requests before closing connection
30
 
31
  # For better performance with Uvicorn
32
+ proc_name = "vibe-coding-rag"