Skip to main content

Project Root

platzi-viewer/
├── 📁 .github/              # GitHub configuration
│   ├── ISSUE_TEMPLATE/      # Bug report and feature request templates
│   ├── workflows/           # CI/CD workflows
│   └── pull_request_template.md
├── 📁 css/                  # Modular stylesheets
│   ├── base/                # Base styles (reset, animations)
│   ├── components/          # Component styles (buttons, cards, navbar)
│   ├── layouts/             # Layout styles (grid, containers)
│   └── views/               # View-specific styles (home, course, player)
├── 📁 js/                   # Frontend application (ES6 modules)
│   ├── 📁 components/       # UI components
│   │   ├── navbar.js        # Navigation bar component
│   │   ├── card.js          # Course card component
│   │   └── modal.js         # Modal dialog component
│   ├── 📁 services/         # Business logic services
│   │   ├── api.js           # API client for backend communication
│   │   ├── state.js         # Global state management
│   │   └── progress.js      # Progress tracking service
│   ├── 📁 views/            # Page views
│   │   ├── home.js          # Home/dashboard view
│   │   ├── explore.js       # Explore categories view
│   │   ├── learning.js      # My Learning progress view
│   │   ├── route.js         # Learning route detail view
│   │   ├── course.js        # Course detail view
│   │   └── player.js        # Video player view
│   ├── app_v2.js            # Application entry point
│   └── router.js            # Hash-based client-side router
├── 🐍 server.py             # Python HTTP server + Drive API proxy
├── 🐍 drive_service.py      # Google Drive API v3 wrapper
├── 🐍 rebuild_cache_drive.py # Cache builder from Drive
├── 🐍 parse_routes.py       # PlatziRoutes.md parser
├── 🐍 desktop_app.py        # Desktop application launcher
├── 🐍 app_launcher.py       # Universal app launcher script
├── 🐍 check_remaining.py    # Course catalog comparison utility
├── 🐍 check_drive_runtime.py # Drive service diagnostic tool
├── 📄 index.html            # Main HTML entry point
├── 🎨 styles.css            # Main stylesheet (imports from css/)
├── 📄 data.js               # Legacy data file (deprecated)
├── 📄 PlatziRoutes.md       # Course catalog definition (Markdown)
├── 📄 courses_cache.json    # Drive-scanned course cache (~20MB)
├── 📄 progress.json         # User progress backup (auto-generated)
├── 📄 service_account.json  # Google service account credentials (gitignored)
├── 📄 drive_scan_progress.json # Cache rebuild resume state (auto-generated)
├── 📄 requirements.txt      # Python dependencies
├── 📄 Dockerfile            # Docker container definition
├── 📄 docker-compose.yml    # Docker Compose configuration
├── 📄 .env.example          # Environment variables template
├── 📄 CONTRIBUTING.md       # Contribution guidelines
├── 📄 README.md             # Project documentation
├── 📄 LICENSE               # MIT License
├── 📄 CHANGELOG.md          # Version history
├── 📄 .gitignore            # Git ignore patterns
├── 📄 .eslintrc.json        # ESLint configuration
├── 📄 .stylelintrc.json     # Stylelint configuration
├── 📄 .dockerignore         # Docker ignore patterns
├── 📄 PlatziViewer.spec     # PyInstaller spec (portable build)
├── 📄 PlatziViewerDesktop.spec # PyInstaller spec (desktop build)
├── 💻 build_portable_exe.ps1    # Windows build script (portable)
└── 💻 build_desktop_exe.ps1     # Windows build script (desktop)

Directory Breakdown

Frontend (js/)

Structure: ES6 modules with component-based architecture

js/components/

Reusable UI components used across multiple views. navbar.js
  • Top navigation bar with category links
  • Search functionality
  • Progress indicator
  • Responsive mobile menu
card.js
  • Course card component
  • Thumbnail, title, progress overlay
  • Click handler for navigation
modal.js
  • Modal dialog wrapper
  • Close on ESC or backdrop click
  • Used for settings, help dialogs

js/services/

Business logic and state management. api.js (ApiService)
class ApiService {
  static async fetchCourses()      // GET /api/courses
  static async saveProgress(data)  // POST /api/progress
  static getVideoUrl(fileId)       // Generate /drive/files/{id}
  static getFileUrl(fileId)        // Generate Drive file URL
}
state.js (state singleton)
const state = {
  courses: null,              // Full course cache
  currentCategory: null,      // Active category
  currentRoute: null,         // Active route
  currentCourse: null,        // Active course
  progress: {},               // User progress map
  searchQuery: '',            // Search filter
  
  // Methods
  init(),
  loadProgress(),
  saveProgress(),
  markClassComplete(courseId, classId),
  getClassProgress(courseId, classId)
}
progress.js
  • Progress calculation utilities
  • Completion percentage
  • Time tracking
  • Sync with localStorage and server

js/views/

Page views mapped to routes.
ViewRouteDescription
home.js#homeDashboard with featured courses
explore.js#exploreBrowse all categories and routes
learning.js#learningUser’s in-progress courses
route.js#route/:idLearning path detail with course list
course.js#course/:idCourse modules and class list
player.js#player/:courseId/:classIdVideo player with resources
View Lifecycle:
export const HomeView = {
  async render() {
    // Return HTML string
  },
  
  afterRender() {
    // Attach event listeners
    // Initialize components
  },
  
  cleanup() {
    // Clean up listeners, timers
  }
}

js/router.js

Hash-based router for SPA navigation.
class Router {
  constructor(routes) {
    this.routes = routes;        // Route -> View mapping
    this.currentView = null;
  }
  
  async navigate(path) {
    // Parse hash path
    // Clean up previous view
    // Render new view
    // Call afterRender()
  }
}
Routes:
  • #homeHomeView
  • #exploreExploreView
  • #learningLearningView
  • #route/:idRouteView
  • #course/:cat/:route/:courseCourseView
  • #player/:courseId/:classIdPlayerView

js/app_v2.js

Application entry point.
import { Router } from './router.js';
import { state } from './services/state.js';
import * as Views from './views/index.js';

const router = new Router({
  '#home': Views.HomeView,
  '#explore': Views.ExploreView,
  // ...
});

await state.init();
router.start();

Stylesheets (css/)

Structure: Modular CSS following BEM methodology

css/base/

_reset.css
  • CSS reset/normalize
  • Box-sizing, margin/padding reset
  • Base typography
_animations.css
  • Keyframe animations
  • Transition utilities
  • Loading spinners, fade effects

css/components/

Component-specific styles.
  • _navbar.css - Navigation bar
  • _card.css - Course cards
  • _button.css - Button variants
  • _modal.css - Modal dialogs
  • _progress.css - Progress bars and indicators

css/layouts/

Layout and grid systems.
  • _grid.css - Responsive grid
  • _container.css - Content containers
  • _sidebar.css - Sidebar layouts

css/views/

View-specific styles.
  • _home.css - Home page
  • _explore.css - Explore page
  • _course.css - Course detail
  • _player.css - Video player
styles.css (main)
@import 'css/base/_reset.css';
@import 'css/base/_animations.css';
@import 'css/components/_navbar.css';
/* ... */

Backend (Python)

Core Server (server.py)

Classes:
  • PlatziHandler(SimpleHTTPRequestHandler) - Request handler
    • do_GET() - Handle GET requests (API, Drive proxy, static files)
    • do_POST() - Handle POST (save progress)
    • do_OPTIONS() - CORS preflight
Functions:
  • init_cache() - Load courses_cache.json into memory
  • refresh_cache_if_changed() - Hot-reload cache on file change
  • get_drive_service() - Lazy-load Drive API wrapper
  • create_server(host, port) - Create ThreadingHTTPServer
  • run_server(server) - Start server loop
Global State:
courses_cache = None           # Full cache data
bootstrap_cache = None         # Lightweight cache (summaries)
cache_meta = {}                # Cache metadata
cache_lock = threading.Lock()  # Thread-safe cache access

# Pre-serialized payloads for performance
full_cache_json_bytes = b""
full_cache_json_gzip_bytes = b""
bootstrap_cache_json_bytes = b""
bootstrap_cache_json_gzip_bytes = b""

Drive API (drive_service.py)

Class: DriveService Methods:
  • authenticate() - Load service account credentials
  • get_service() - Get thread-local Drive API client
  • list_files(folder_id) - List folder contents with pagination
  • get_file_metadata(file_id) - Get file info (name, size, MIME)
  • download_file_range(file_id, range_header) - Stream file with Range support
Thread Safety:
class DriveService:
    def __init__(self):
        self._thread_local = threading.local()
        self._shared_session = None
        self._shared_session_lock = threading.Lock()

Cache Builder (rebuild_cache_drive.py)

Functions:
  • sanitize_for_match(name) - Normalize course names for fuzzy matching
  • list_drive_folder(folder_id) - List Drive folder with throttling
  • scan_drive_classes(folder_id) - Scan module folder for class files
  • scan_drive_course(folder_id, name) - Scan entire course structure
  • match_course_to_drive(md_name, drive_map) - Fuzzy match course names
  • load_scan_progress() / save_scan_progress() - Resume capability
Flow:
  1. Parse PlatziRoutes.md → Expected courses
  2. List Drive root → Available folders
  3. Match courses to folders (fuzzy)
  4. Scan each course → modules → classes → files
  5. Store Drive file IDs
  6. Save to courses_cache.json

Route Parser (parse_routes.py)

Functions:
  • parse(filepath) - Parse PlatziRoutes.md into JSON
  • sanitize_folder_name(name) - Match Drive folder naming
  • print_summary(data) - Display statistics
Output:
{
  'categories': [...],
  'stats': {
    'totalCategories': 17,
    'totalRoutes': 156,
    'totalCourses': 542
  }
}

Data Files

PlatziRoutes.md

Format: Markdown with hierarchical structure
## School: Desarrollo Web

### [Fundamentos de Programación](https://platzi.com/ruta/...)

- [Curso de Programación Básica](https://platzi.com/cursos/programacion-basica/)
- [Curso de Python](https://platzi.com/cursos/python/)

### [Desarrollo Frontend](https://platzi.com/ruta/...)

- [Curso de HTML y CSS](https://platzi.com/cursos/html-css/)
Structure:
  • ## School: → Category
  • ### [Route](url) → Learning path
  • - [Course](url) → Individual course

courses_cache.json

Size: ~20MB
Format: JSON
Structure: See Scripts - rebuild_cache_drive.py
Purpose: Complete course catalog with Drive file IDs
Generated by: rebuild_cache_drive.py
Used by: server.py (loaded at startup)

progress.json

Size: Varies (typically less than 1MB)
Format: JSON
Structure:
{
  "curso-python": {
    "clase-1": {
      "completed": true,
      "timestamp": 1678901234567,
      "watchTime": 847
    }
  }
}
Generated by: User interaction (auto-saved)
Used by: Frontend progress tracking

service_account.json

Size: ~2KB
Format: JSON (Google service account key)
Security: MUST BE GITIGNORED
Purpose: Authenticate with Google Drive API
Structure:
{
  "type": "service_account",
  "project_id": "...",
  "private_key_id": "...",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
  "client_email": "[email protected]",
  "client_id": "...",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token"
}

Configuration Files

.env.example

Purpose: Environment variables template
# Server
PORT=8080
HOST=127.0.0.1

# Paths
PLATZI_VIEWER_PATH=/path/to/app
PLATZI_DATA_PATH=/path/to/data

# Google Drive
GOOGLE_SERVICE_ACCOUNT_FILE=/path/to/service_account.json

requirements.txt

Python dependencies:
google-api-python-client>=2.70.0
google-auth>=2.15.0
google-auth-httplib2>=0.1.0
PyQt6>=6.4.0
PyQt6-WebEngine>=6.4.0
pywebview>=3.7
pyinstaller>=5.7.0

.eslintrc.json

JavaScript linting:
{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": "eslint:recommended",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  }
}

.stylelintrc.json

CSS linting:
{
  "extends": "stylelint-config-standard",
  "rules": {
    "indentation": 2,
    "string-quotes": "single"
  }
}

Build Configuration

Dockerfile

Purpose: Containerize the application
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["python", "server.py"]

docker-compose.yml

Purpose: Multi-service orchestration
services:
  platzi-viewer:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ./secrets:/app/secrets:ro
      - ./runtime-data:/app/data
    environment:
      - GOOGLE_SERVICE_ACCOUNT_FILE=/app/secrets/service_account.json

PlatziViewer.spec

Purpose: PyInstaller configuration for portable EXE
a = Analysis(
    ['server.py'],
    pathex=[],
    binaries=[],
    datas=[
        ('index.html', '.'),
        ('js', 'js'),
        ('css', 'css'),
        ('courses_cache.json', '.'),
    ],
    hiddenimports=['google.auth', ...],
)

PlatziViewerDesktop.spec

Purpose: PyInstaller configuration for desktop app
a = Analysis(
    ['desktop_app.py'],
    # ... includes PyQt6 and WebEngine
)

File Interactions


Key Conventions

Naming

  • Python: snake_case for functions, PascalCase for classes
  • JavaScript: camelCase for variables/functions, PascalCase for classes
  • CSS: kebab-case with BEM (.block__element--modifier)
  • Files: lowercase-with-hyphens.ext

Imports

JavaScript (ES6):
import { ApiService } from './services/api.js';
import { state } from './services/state.js';
Python:
from drive_service import drive_service
import parse_routes

Comments

JavaScript:
/**
 * Navigate to course detail view
 * @param {string} courseId - Course identifier
 */
function navigateToCourse(courseId) { ... }
Python:
def scan_drive_course(folder_id, name):
    """Scan a course folder in Drive for modules/classes.
    
    Args:
        folder_id: Google Drive folder ID
        name: Course folder name
        
    Returns:
        Tuple of (modules_list, has_presentation, presentation_id)
    """

Security

Gitignored Files:
  • service_account.json - NEVER COMMIT
  • courses_cache.json - Optional (large)
  • progress.json - User-specific
  • drive_scan_progress.json - Temporary
  • *.pyc, __pycache__/ - Python bytecode
  • .env - Environment secrets
  • dist/, build/ - Build artifacts
Sensitive Data:
  • Service account credentials
  • User progress data
  • Drive file IDs (not secret but should be regenerated)
Never commit service_account.json. Share it only through secure channels (encrypted email, password manager, etc.).

Build docs developers (and LLMs) love