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.
| View | Route | Description |
|---|
home.js | #home | Dashboard with featured courses |
explore.js | #explore | Browse all categories and routes |
learning.js | #learning | User’s in-progress courses |
route.js | #route/:id | Learning path detail with course list |
course.js | #course/:id | Course modules and class list |
player.js | #player/:courseId/:classId | Video 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:
#home → HomeView
#explore → ExploreView
#learning → LearningView
#route/:id → RouteView
#course/:cat/:route/:course → CourseView
#player/:courseId/:classId → PlayerView
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:
- Parse
PlatziRoutes.md → Expected courses
- List Drive root → Available folders
- Match courses to folders (fuzzy)
- Scan each course → modules → classes → files
- Store Drive file IDs
- 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
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.).