Skip to main content

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.
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:
  1. 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
  2. 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
  3. 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:
1

Frontend requests video URL

const videoUrl = ApiService.getVideoUrl(classData.files.video);
// Returns: http://localhost:8080/drive/files/1OOJ5lrsLfFEnp6AKVKZKYZH5A-NasCjl
videoElement.src = videoUrl;
2

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)
3

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:
When persistent A/V drift is detected:
  1. Automatic detection: After multiple resync attempts, suggest compatibility mode
  2. User activation: Click “Try Compatibility Mode” in the prompt
  3. Backend processing: Server switches to /api/video-compatible/{fileId} endpoint
  4. 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
    
  5. 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:
KeyAction
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:
const classStatus = state.getClassStatus(classKey);
// Returns: 'not_started' | 'in_progress' | 'complete'

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 .\build_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.2s ease, box-shadow 0.2s ease;
}

.course-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 24px 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:
EndpointMethodDescriptionResponse
/api/bootstrapGETLightweight course metadata (names, counts)JSON (~200KB)
/api/coursesGETComplete course structure with all detailsJSON (~20MB)
/api/course-detail/{cat}/{route}/{course}GETDetailed info for a single courseJSON
/api/cache-metaGETCache statistics and health checkJSON
/api/progressGETUser progress dataJSON
/api/progressPOSTSave user progress204 No Content
/api/refreshGETReload cache from disk (localhost only)JSON
/api/healthGETServer health, Drive status, FFmpeg statusJSON
/api/video-compatible/{fileId}GETFFmpeg-processed video streamvideo/mp4
/drive/files/{fileId}GETProxy 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.

Performance Optimizations

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

Build docs developers (and LLMs) love