Content Management & Navigation
Hierarchical Course Structure
Navigate through courses with a five-level hierarchy that mirrors how Platzi organizes learning content:
Categories (e.g., "Web Development", "Data Science")
└── Learning Paths / Routes
└── Courses
└── Modules
└── Classes (individual lessons)
Each level provides context and organization:
Categories : High-level topics with custom icons
Routes : Curated learning paths or standalone courses
Courses : Complete course units with modules
Modules : Thematic groupings of related classes
Classes : Individual video lessons with supporting materials
The structure is defined in PlatziRoutes.md and matched against your Google Drive folder organization during cache building.
Advanced Search
Find courses and classes instantly with the search functionality:
// From js/services/state.js
search ( query ) {
const results = [];
categories . forEach (( cat , catIdx ) => {
cat . routes . forEach (( route , routeIdx ) => {
if ( route . name . toLowerCase (). includes ( query )) {
results . push ({ type: 'route' , item: route , catIdx , routeIdx });
}
if ( ! route . isCourse && route . courses ) {
route . courses . forEach (( course , courseIdx ) => {
if ( course . name . toLowerCase (). includes ( query )) {
results . push ({ type: 'course' , item: course , catIdx , routeIdx , courseIdx });
}
});
}
});
});
return results ;
}
Features:
Case-insensitive matching : Find content regardless of capitalization
Multi-level results : Returns both routes and individual courses
Context preservation : Results include category and route information
Real-time feedback : Instant results as you type
Content Type Filtering
Each class can have multiple resource types, all detected and categorized during cache building:
Video .mp4 files streamed directly from Google Drive with Range Request support
Summary *_summary.html files containing lesson notes and key takeaways
Reading .txt files with recommended readings and additional materials
Sandbox/HTML .html files for interactive exercises and code demonstrations
The cache builder automatically detects file types based on naming patterns:
# From rebuild_cache_drive.py
def classify_file ( filename ):
lower_name = filename.lower()
if lower_name.endswith( '_summary.html' ):
return 'summary'
elif lower_name.endswith( '.vtt' ):
return 'subtitles'
elif lower_name.endswith( '.mp4' ):
return 'video'
elif lower_name.endswith( '.txt' ) and 'lectura' in lower_name:
return 'reading'
elif lower_name.endswith( '.html' ):
return 'html'
else :
return 'resource'
Lazy Loading Architecture
To optimize performance with large course catalogs (~20,000 classes), Platzi Viewer uses a multi-tier loading strategy:
Bootstrap Load (/api/bootstrap):
Lightweight metadata only: category names, route names, class counts
No module or class details
~200KB instead of ~20MB
Loads instantly on application start
On-Demand Course Details (/api/course-detail/{indices}):
Full module structure and class lists loaded when user navigates to a course
Cached in memory after first load
Prevents loading unused course data
File Streaming (/drive/files/{fileId}):
Videos and files streamed directly from Google Drive
Only requested content is transferred
No local storage of course materials
// From js/services/state.js
async ensureCourseDetail ( catIdx , routeIdx , courseIdx ) {
const course = this . getCourse ( catIdx , routeIdx , courseIdx );
// Check if already loaded
if ( this . isCourseDetailLoaded ( course )) {
return course ;
}
// Fetch detail from server
const detail = await ApiService . getCourseDetail ( catIdx , routeIdx , courseIdx );
// Update local cache
this . updateCourseInCache ( catIdx , routeIdx , courseIdx , detail );
return this . getCourse ( catIdx , routeIdx , courseIdx );
}
Video Streaming & Playback
Direct Google Drive Streaming
All videos stream in real-time from Google Drive without downloading:
Frontend requests video URL
const videoUrl = ApiService . getVideoUrl ( classData . files . video );
// Returns: http://localhost:8080/drive/files/1OOJ5lrsLfFEnp6AKVKZKYZH5A-NasCjl
videoElement . src = videoUrl ;
Backend proxies to Google Drive
# server.py handles /drive/files/{fileId}
file_metadata = drive_service.get_file_metadata(file_id)
stream = drive_service.download_file_range(file_id, start_byte, end_byte)
Chunks streamed to browser
Content flows from Drive → Python backend → Browser in 1MB chunks
No videos are stored locally. The backend acts as a transparent proxy with Range Request support for efficient seeking.
HTTP Range Request Support
Enables smooth video seeking without downloading the entire file:
Browser Request:
GET /drive/files/ABC123
Range: bytes=0-1048575
Server Response:
HTTP 206 Partial Content
Content-Range: bytes 0-1048575/52428800
Content-Type: video/mp4
[1MB chunk of video data...]
Benefits:
Instant seeking : Jump to any point in the video without buffering the entire file
Bandwidth efficiency : Only download the portions you watch
Standard browser support : Works with native HTML5 <video> elements
Audio/Video Synchronization
Platzi Viewer includes sophisticated A/V sync monitoring and correction:
// From js/views/player.js - Frame-level sync monitoring
video . requestVideoFrameCallback (( now , metadata ) => {
const drift = this . _calculateAudioVideoDrift ( video );
if ( Math . abs ( drift ) > SOFT_CORRECTION_THRESHOLD ) {
// Soft correction: Adjust playback rate slightly
video . playbackRate = drift > 0 ? 1.02 : 0.98 ;
}
if ( Math . abs ( drift ) > HARD_CORRECTION_THRESHOLD ) {
// Hard correction: Pause and resync
video . pause ();
video . currentTime += drift / 1000 ;
video . play ();
}
// Continue monitoring
this . _videoFrameCallbackId = video . requestVideoFrameCallback ( ... );
});
Features:
Frame-level monitoring : Uses requestVideoFrameCallback API for precise drift detection
Adaptive correction : Soft adjustments via playback rate, hard resyncs for severe drift
Statistics tracking : Exposes window.__platziAvSyncLastStats for debugging
Compatibility mode : Automatic fallback to FFmpeg-based streaming for problematic files
Compatibility Mode with FFmpeg
For videos with damaged timestamps or encoding issues, Platzi Viewer can re-encode on-the-fly:
How Compatibility Mode Works
When persistent A/V drift is detected:
Automatic detection : After multiple resync attempts, suggest compatibility mode
User activation : Click “Try Compatibility Mode” in the prompt
Backend processing : Server switches to /api/video-compatible/{fileId} endpoint
FFmpeg remux/transcode :
ffmpeg -i input.mp4 -c copy -avoid_negative_ts make_zero output.mp4
# Or full re-encode if timestamps are severely damaged:
ffmpeg -i input.mp4 -c:v libx264 -c:a aac output.mp4
Streaming : Processed video streamed to browser
The system tracks compatibility mode statistics at /api/health: {
"compatStream" : {
"totalRequests" : 15 ,
"successfulStreams" : 14 ,
"failedStreams" : 1 ,
"lastMode" : "copy" ,
"lastSpeedMBps" : 3.45 ,
"lastDurationSec" : 42.3
}
}
Compatibility mode requires FFmpeg to be installed on the server. Without it, only direct Drive streaming is available.
Keyboard Navigation
Efficient class navigation without touching the mouse:
Key Action ←Previous class →Next class SpacePlay/Pause FToggle fullscreen EscExit player
Clean Resource Management
The player properly cleans up when navigating away:
// From js/views/player.js
destroy () {
this . _isDestroyed = true ;
// Stop and unload video
if ( this . _videoEl ) {
this . _videoEl . pause ();
this . _videoEl . src = '' ;
this . _videoEl . load (); // Force release of resources
}
// Cancel frame callbacks
if ( this . _videoFrameCallbackId ) {
this . _videoEl . cancelVideoFrameCallback ( this . _videoFrameCallbackId );
}
// Clear timers
clearTimeout ( this . _startPlaybackTimeout );
clearTimeout ( this . _syncPromptHideTimeout );
}
This ensures no residual audio or memory leaks when switching between classes.
Progress Tracking & Analytics
Dual-Layer Persistence
Progress is saved in two places for redundancy:
Browser localStorage Instant local saves on every progress change. Survives browser restarts but is machine-specific.
Server progress.json Debounced sync to server after 400ms. Survives across machines and can be backed up.
Progress States
Each class has one of three states:
// From js/services/state.js
const progress = {
'not_started' : {
// No entry in progress object
},
'in_progress' : {
status: 'in_progress' ,
lastWatched: '2026-03-07T10:30:00.000Z' ,
watchTime: 245 // seconds watched
},
'complete' : {
status: 'complete' ,
completedAt: '2026-03-07T11:15:00.000Z'
}
};
Automatic Completion
Classes are automatically marked complete when the user watches 90% or more:
// From js/views/player.js
video . addEventListener ( 'timeupdate' , () => {
const progress = video . currentTime / video . duration ;
if ( progress >= 0.9 && ! this . isClassComplete ( classKey )) {
state . markClassComplete ( classKey );
this . updateProgressIndicators ();
}
});
Progress Merging Strategy
When syncing local and server progress, the system uses intelligent merge logic:
// From js/services/state.js
mergeProgress ( localProgress , serverProgress ) {
const merged = {};
Object . keys ( allKeys ). forEach ( classKey => {
const local = localProgress [ classKey ];
const server = serverProgress [ classKey ];
// Priority 1: Highest completion status wins
if ( local . status === 'complete' || server . status === 'complete' ) {
merged [ classKey ] = local . status === 'complete' ? local : server ;
}
// Priority 2: Most recent timestamp wins
else {
const localTime = Date . parse ( local . lastWatched || local . completedAt );
const serverTime = Date . parse ( server . lastWatched || server . completedAt );
merged [ classKey ] = serverTime >= localTime ? server : local ;
}
});
return merged ;
}
Granular Statistics
Track progress at multiple levels:
Class Level
Course Level
Route Level
Overall
const classStatus = state . getClassStatus ( classKey );
// Returns: 'not_started' | 'in_progress' | 'complete'
const courseProgress = state . getCourseProgress ( catIdx , routeIdx , courseIdx );
// Returns: 0.67 (67% complete)
const total = state . countCourseClasses ( course ); // 45 classes
const completed = state . countCourseCompleted ( catIdx , routeIdx , courseIdx ); // 30 classes
const routeProgress = state . getRouteProgress ( catIdx , routeIdx );
// Returns: { completed: 120, total: 200, percent: 0.6 }
const overall = state . getOverallProgress ();
// Returns: {
// totalClasses: 20000,
// completedClasses: 3500,
// totalRoutes: 450,
// startedRoutes: 75,
// completedRoutes: 15,
// percent: 0.175
// }
Learning Dashboard
The Learning view (#learning) shows all courses with progress:
Courses in progress : Routes with at least one completed class
Completion percentage : Visual progress bars
Time statistics : Last watched dates (when implemented)
Resume functionality : Quick access to continue where you left off
Desktop Application
Native Desktop Experience
Platzi Viewer can run as a standalone desktop application:
# Build desktop executable (Windows)
powershell -ExecutionPolicy Bypass -File . \b uild_desktop_exe.ps1
# Output: dist/PlatziViewerDesktop.exe
The desktop app bundles:
Python backend server
Complete frontend (HTML/JS/CSS)
PyQt6 or pywebview UI framework
Course cache (optional)
Service account credentials (optional)
Desktop Architecture
# From desktop_app.py
def main ():
# Configure GPU acceleration
configure_gpu_acceleration()
# Find free port and start server
port = get_free_port()
server = create_server( '127.0.0.1' , port)
# Start server in background thread
server_thread = threading.Thread( target = run_server, args = (server,))
server_thread.start()
# Create native window
if QWebEngineView is available:
window = create_qt_window( f 'http://127.0.0.1: { port } ' )
elif pywebview is available:
window = webview.create_window( 'Platzi Viewer' , url)
# Show window and handle shutdown
window.show()
window.on_closed( lambda : server.shutdown())
GPU Acceleration
The desktop app automatically enables hardware video decoding:
# From desktop_app.py
def _configure_gpu_acceleration ():
chromium_gpu_flags = [
"--ignore-gpu-blocklist" ,
"--enable-gpu-rasterization" ,
"--enable-zero-copy" ,
"--enable-accelerated-video-decode" ,
"--enable-native-gpu-memory-buffers" ,
]
os.environ[ "QTWEBENGINE_CHROMIUM_FLAGS" ] = " " .join(chromium_gpu_flags)
On Windows, you can force the app to use your dedicated GPU: Settings → System → Display → Graphics → Add App → Select PlatziViewerDesktop.exe → High Performance
Persistent Storage
The desktop app maintains a dedicated data directory:
PlatziViewerDesktop.exe
PlatziData/
├── progress.json # User progress
├── courses_cache.json # Course metadata (optional)
├── service_account.json # Drive credentials
├── QtWebEngine/ # Browser cache
│ ├── localStorage/
│ └── IndexedDB/
└── drive_scan_progress.json # Resume state for cache rebuilds
Portable Executable
Create a fully portable version that includes everything:
# Build portable executable
.\build_portable_exe.ps1
# Creates: dist/PlatziViewer/
# ├── PlatziViewer.exe
# ├── courses_cache.json
# ├── service_account.json (if included)
# └── [Python runtime libraries]
This can be zipped and shared with family members for instant setup.
Modern User Interface
Responsive Design
The interface adapts seamlessly from desktop to mobile:
Fluid layouts : CSS Grid and Flexbox for flexible sizing
Breakpoints : Optimized views for desktop (1024px+), tablet (768px+), and mobile (320px+)
Touch-friendly : Larger hit targets and swipe gestures on mobile devices
Dark Mode
Reduce eye strain with a carefully crafted dark theme:
/* From styles.css */
:root {
--bg-primary : #0a0e27 ;
--bg-secondary : #131833 ;
--bg-card : #1a1f3a ;
--text-primary : #e2e8f0 ;
--text-secondary : #94a3b8 ;
--text-muted : #64748b ;
--accent-primary : #60a5fa ;
--accent-hover : #3b82f6 ;
}
Smooth Animations
Micro-interactions enhance the experience:
Card hover effects : Subtle elevation and scale transforms
Route transitions : Fade and slide animations between views
Progress indicators : Animated progress bars with easing
Loading states : Smooth spinner animations
/* Example: Card hover animation */
.course-card {
transition : transform 0.2 s ease , box-shadow 0.2 s ease ;
}
.course-card:hover {
transform : translateY ( -4 px );
box-shadow : 0 8 px 24 px rgba ( 0 , 0 , 0 , 0.3 );
}
Component-Based Architecture
The frontend uses a modular component system:
// From js/components/navbar.js
export const Navbar = {
render () {
return `
<nav class="navbar">
<div class="navbar-brand">
<span class="navbar-icon">🎓</span>
<span class="navbar-title">Platzi Viewer</span>
</div>
<div class="navbar-links">
${ this . renderLinks () }
</div>
</nav>
` ;
},
renderLinks () {
const links = [
{ hash: '#home' , icon: '🏠' , label: 'Inicio' },
{ hash: '#explore' , icon: '🔍' , label: 'Explorar' },
{ hash: '#learning' , icon: '📚' , label: 'Mi Aprendizaje' }
];
return links . map ( link => `
<a href=" ${ link . hash } " class="navbar-link" data-hash=" ${ link . hash } ">
<span class="navbar-link-icon"> ${ link . icon } </span>
<span class="navbar-link-label"> ${ link . label } </span>
</a>
` ). join ( '' );
}
};
API Endpoints
Platzi Viewer exposes several REST API endpoints:
Endpoint Method Description Response /api/bootstrapGET Lightweight course metadata (names, counts) JSON (~200KB) /api/coursesGET Complete course structure with all details JSON (~20MB) /api/course-detail/{cat}/{route}/{course}GET Detailed info for a single course JSON /api/cache-metaGET Cache statistics and health check JSON /api/progressGET User progress data JSON /api/progressPOST Save user progress 204 No Content /api/refreshGET Reload cache from disk (localhost only) JSON /api/healthGET Server health, Drive status, FFmpeg status JSON /api/video-compatible/{fileId}GET FFmpeg-processed video stream video/mp4 /drive/files/{fileId}GET Proxy to Google Drive file (file content)
The /api/refresh endpoint is restricted to localhost to prevent remote cache reloads in shared deployments.
Security Features
Read-Only Drive Access
The Google Drive service account uses minimal permissions:
# From drive_service.py
SCOPES = [ 'https://www.googleapis.com/auth/drive.readonly' ]
This ensures the application cannot:
Modify existing files
Delete files or folders
Share folders with others
Change permissions
Localhost Restrictions
Sensitive operations are restricted to loopback access:
# From server.py
LOOPBACK_HOSTS = { "localhost" , "127.0.0.1" , "::1" }
def do_GET ( self ):
if self .path == '/api/refresh' :
# Only allow from localhost
client_host = self .client_address[ 0 ]
if client_host not in LOOPBACK_HOSTS :
self .send_error( 403 , "Refresh only allowed from localhost" )
return
Progress Payload Validation
The server validates progress data size to prevent abuse:
# From server.py
MAX_PROGRESS_BYTES = 2 * 1024 * 1024 # 2MB limit
def do_POST ( self ):
if self .path == '/api/progress' :
content_length = int ( self .headers.get( 'Content-Length' , 0 ))
if content_length > MAX_PROGRESS_BYTES :
self .send_error( 413 , "Progress payload too large" )
return
Credential Handling
Service account credentials are never exposed to the frontend:
Backend loads credentials from file or environment variable
Frontend never receives authentication tokens
All Drive API calls proxied through backend
When sharing the portable app, be cautious about including service_account.json. Consider distributing it separately via a secure channel.
Threaded HTTP Server
The backend uses ThreadingHTTPServer to handle concurrent requests:
# From server.py
from http.server import ThreadingHTTPServer
server = ThreadingHTTPServer((host, port), PlatziHandler)
Benefits:
Concurrent streaming : Multiple users or tabs can stream different videos simultaneously
Non-blocking operations : File downloads don’t block API requests
Scalability : Handles multiple family members accessing the server
Gzip Compression
API responses are automatically compressed when the client supports it:
# From server.py
def send_json_response ( self , data ):
json_bytes = json.dumps(data).encode( 'utf-8' )
if 'gzip' in self .headers.get( 'Accept-Encoding' , '' ):
compressed = gzip.compress(json_bytes)
self .send_header( 'Content-Encoding' , 'gzip' )
self .send_header( 'Content-Length' , len (compressed))
self .wfile.write(compressed)
else :
self .send_header( 'Content-Length' , len (json_bytes))
self .wfile.write(json_bytes)
This reduces the 20MB full course cache to ~2-3MB when transferred.
Chunked Streaming
Videos and files are streamed in 1MB chunks to minimize memory usage:
# From drive_service.py
CHUNK_SIZE = 1024 * 1024 # 1MB
def download_file_range ( file_id , start , end ):
request = service.files().get_media( fileId = file_id)
request.headers[ 'Range' ] = f 'bytes= { start } - { end } '
while True :
chunk = request.read( CHUNK_SIZE )
if not chunk:
break
yield chunk
Resume Capability for Cache Building
The cache builder can resume interrupted scans:
# From rebuild_cache_drive.py
def save_progress ( state ):
with open ( 'drive_scan_progress.json' , 'w' ) as f:
json.dump(state, f)
def load_progress ():
if os.path.exists( 'drive_scan_progress.json' ):
with open ( 'drive_scan_progress.json' ) as f:
return json.load(f)
return None
# Resume from saved state
state = load_progress()
if state:
print ( f "Resuming from course { state[ 'last_course_index' ] } ..." )
This prevents losing hours of scanning progress if the process is interrupted.
Next Steps
Explore specific topics in depth:
Installation Guide Set up your development environment and configure Google Drive access
Building the Cache Scan your Google Drive and generate the course metadata cache
Configuration Customize server settings, environment variables, and deployment options
Desktop App Build and distribute the standalone desktop application