Files
offlineu/README.md
T
rmsitzandClaude Sonnet 5 4eda493e00 Add Unsorted folder auto-sort with keyword matching
Scans an Unsorted folder at the library root and proposes destinations
for new courses by matching keywords against the existing category
tree, suggesting new subfolders when nothing matches closely, and
flagging items for manual review when nothing matches at all. Review
and apply happen on a dedicated /unsorted page linked from Settings;
nothing moves until the user confirms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 09:10:19 -04:00

227 lines
9.8 KiB
Markdown

# OfflineU
Self-hosted course viewer for a local video/audio/text training library. Runs
as a single Flask app in Docker, tracks watch progress and notes per lesson,
and gives the whole library a dashboard-style home page instead of a bare
file browser.
This is a personal deployment, not a public project — this doc is internal
reference for running/maintaining it, not a pitch.
![Lesson view](images/lesson-0-8-2025-08-04-04_58_17.png)
*Screenshot predates the current theming/icon work — kept for a rough idea
of layout, not pixel-accurate.*
---
## Deployment
Deployed via `docker-compose.yml`, built directly from the `Dockerfile` in
this repo (`pull_policy: build`, not a registry pull):
```yaml
services:
offlineu:
build: .
pull_policy: build
container_name: offlineu
network_mode: host
ports:
- "5000:5000"
environment:
- PUID=1000
- PGID=10
- TZ=America/New_York
- FLASK_ENV=production
volumes:
- /volume1/files/training:/app/courses # course library
- /volume2/docker/offlineu/data:/app/data # settings/progress/notes
restart: unless-stopped
```
**Auto-deploy:** the Gitea repo has a webhook to Dockhand — a push to `main`
triggers a full image **rebuild** from the Dockerfile (confirmed, not just a
container restart), so Dockerfile changes (e.g. adding `ffmpeg`) take effect
on the very next push without any manual step.
---
## Local development
```bash
pip install -r requirements.txt
python offlineu_core.py --library-path /path/to/courses --debug
```
Opens on `http://127.0.0.1:5000`. `ffprobe` (from ffmpeg) needs to be on
`PATH` for video-duration lookups to work locally; without it those just
silently stay blank instead of erroring.
### CLI options
| Option | Description |
| ---------------------- | ----------------------------------------------------- |
| `--host` | Bind host (default `0.0.0.0`) |
| `--port` | Bind port (default `5000`) |
| `--debug` | Enable Flask debug mode |
| `--create-templates` | Regenerate default templates if missing |
| `--library-path` | Course library root (overrides `COURSES_LIBRARY_PATH`) |
| `course_path` (positional) | Load a specific course directly at startup |
### Environment variables
| Variable | Default | Purpose |
| ---------------------- | -------------- | ------------------------------------------ |
| `COURSES_LIBRARY_PATH` | `/app/courses` | Course library root |
| `OFFLINEU_DATA_DIR` | `/app/data` | Where settings/progress/notes data lives |
| `AUTO_LOAD_COURSE` | — | Course path to load automatically on start |
---
## Features
**Library browsing**
- Lazy-loading folder browser (grid or list view) with search, including an
opt-in transcript search across subtitle files
- Course cards show media file count, total video/audio runtime, and
completion % at a glance
- Cover art: uses a manually-placed `cover`/`folder`/`thumbnail`/`thumb`/
`poster` image if a course has one, otherwise auto-generates one via
`ffmpeg` from a frame of the course's first video
- Hide courses/folders from the browser without touching anything on disk;
bulk-select to hide or queue several at once
- Bulk rename across course/folder names (find & replace)
- **Sort Unsorted** ([/unsorted](templates/unsorted.html), linked from
Settings): drop new/incoming courses into an `Unsorted` folder at the
library root, then scan proposes a destination for each one by keyword
overlap against the *existing* category tree - read fresh from disk every
scan, so it adapts to whatever folders you actually have rather than any
hardcoded subject list. Three outcomes per item: a confident match to an
existing folder, a suggested new subfolder under a broader category match,
or "needs review" (unchecked by default) when nothing overlaps at all.
Every destination is editable before applying, and nothing on disk moves
until you review and hit Apply.
**Dashboard**
- Library-wide stats (courses, lessons completed, time watched, time
remaining, day streak) and a 90-day activity heatmap
- Next Up queue (manually curated, reorderable)
- Pick Back Up (courses with progress that have gone stale)
- Recently Added / Recently Viewed
- Surprise Me — dice icon in the header, random pick weighted toward
incomplete courses
**Playback & progress**
- Video/audio player with resize, playback-speed presets, and resume-from-
last-position
- Auto-tracks watch progress and completion per lesson
- Video/audio durations read via `ffprobe` and cached persistently per
course (`.offlineu_duration_cache.json`), so total runtime and per-lesson
length show up before you've ever pressed play — not just after; the
same cache backs the library-wide "time remaining" stat
- Settings → "Precompute Lengths & Cover Art" walks the whole library in
the background to populate durations and thumbnails up front, with live
progress
**Notes**
- Timestamped notes per lesson, capturable via a keyboard shortcut without
leaving the player
- Notes Hub: every note across the whole library, searchable, in one list
- Per-course study guide export (markdown, organized by section)
**Personalization** (Settings)
- 46 built-in themes (grouped in the dropdown: Base, Editor Themes, and 5
categories of website-inspired schemes sourced from Figma's "53 Unique
Website Color Schemes" - 34 of the 53 kept, all as dark/colored
backgrounds rather than the source page's mostly-white mockups, after
cutting a couple of neon-bright accents and pruning near-duplicates by
actual color distance), or a fully custom accent color, corner radius,
and card style
- Font, text size, page width, and spacing density
- Default library path override + manual "Refresh Library" (bypasses the
5-minute filesystem-scan cache)
**Backup & integrations**
- One-click backup export and restore: settings, hidden-path choices, the
Next Up queue, recent-view history, Outline config, and every course's
progress/notes as a zip (not the course files themselves). Restore
overwrites current data and needs a matching course folder to already
exist for each course's progress to land - it's a "put my data back"
action, not a merge
- Optional [Outline](https://www.getoutline.com/) integration — push a
lesson's notes to an Outline document/collection
---
## Data files
Everything under `OFFLINEU_DATA_DIR` (app-wide, not tied to a course):
| File | Contents |
| ---------------------- | ------------------------------------------ |
| `settings.json` | Theme, layout, library path, etc. |
| `hidden_paths.json` | Courses/folders curated out of the browser |
| `next_up.json` | The Next Up queue, in order |
| `recent_views.json` | Cross-course "Recently Viewed" history |
| `outline_config.json` | Outline API token/collection mapping |
Per-course, written inside the course's own folder on the library volume:
| File | Contents |
| ----------------------------------- | -------------------------------------------------------- |
| `.offlineu_progress.json` | Per-lesson completed/progress/duration/notes |
| `.offlineu_duration_cache.json` | ffprobe duration cache, keyed by (path, size, mtime) |
| `.offlineu_thumbnail.jpg` | Auto-generated cover art (only if no manual cover exists) |
---
## Folder structure example
```
MyCourse/
├── Section 1/
│ ├── 01 - Intro.mp4
│ ├── 02 - Setup Guide.pdf
│ └── 03 - Quiz.html
├── Section 2/
│ ├── 04 - Advanced Tips.mp4
│ └── resources/
│ └── extras.md
├── .offlineu_progress.json ← created automatically
├── .offlineu_duration_cache.json ← created automatically
└── .offlineu_thumbnail.jpg ← created automatically, only if no manual cover exists
```
No metadata files needed — course/section/lesson names come straight from
folder and file names.
An `Unsorted/` folder at the library root (alongside the real category
folders) is the drop point for new/incoming courses — see Sort Unsorted
above.
## Supported file types
| Type | Extensions |
| --------- | ------------------------------------------------------------------ |
| Video | `.mp4` `.mkv` `.avi` `.mov` `.webm` `.m4v` `.flv` `.wmv` |
| Audio | `.mp3` `.wav` `.m4a` `.aac` `.ogg` `.flac` |
| Documents | `.txt` `.md` `.html` `.htm` `.pdf` `.docx` `.doc` `.rtf` |
| Subtitles | `.srt` `.vtt` `.ass` `.sub` `.sbv` |
| Quizzes | Any doc file whose name contains `quiz`, `exam`, `test`, `assessment`, `exercise`, `assignment`, or `homework` |
---
## Tools
`tools/generate_website_scheme_themes.py` is the one-off script that
produced the 34 "website scheme" theme entries in `THEME_PALETTES` (of
53 candidates - all 24 light-mode ones were converted to dark/colored
backgrounds per request, then 19 were cut across two review passes: 2
for a neon-bright accent, 17 for landing too close to another kept
scheme once mapped into actual UI colors, since forcing everything dark
made several collide that hadn't before) - not part of the running app,
kept for reference if those palettes ever need regenerating or
extending. `build_theme()` takes a `force_mood='dark'|'light'` override
for exactly this kind of bulk remapping. See its module docstring for
how it turned Figma's color-scheme screenshots into actual UI palettes.