Vì sao thiết kế thế này. Schema chính xác xem
spec/contract.yaml; lớp proxy xemdocs/proxy.md.
Đẩy mọi logic về build-time (ingest) + client; CDN chỉ serve byte tĩnh. Không app server trên đường đọc → scale theo GB, không theo user; chi phí ≈ chỉ storage. Một ngoại lệ hẹp: proxy edge cho byte phim ở host cần CORS/Range — vẫn edge-only, không phải app server đọc data.
- Write ⟂ Read — bộ ghi (crawler) và bộ đọc (client) gặp nhau CHỈ qua file tĩnh R2.
- Data ⟂ Blob — R2 = data + token (
gd:/r2:/b2:/src:); byte phim ở host khác, đổi host không đụng data. - Mutable ⟂ Immutable — chỉ pointer nhỏ tươi (SWR); payload
*.v{rev}immutable, cache mãi. - Direct ⟂ Proxied —
r2/b2thẳng;gd/srcqua proxy/p/*. Data/v1/*.jsonkhông bao giờ qua proxy.
| Lớp | Chọn | Server? |
|---|---|---|
| Data | R2 custom domain (10GB free ≈ hàng chục nghìn title) | không |
| CDN | Cloudflare Cache Rules | không |
| Frontend | Astro static (Pages) | không |
| Ingestion | GitHub Actions cron (transcode/sub) | ephemeral |
| Blob video | R2 / B2 / Drive (1 file/tập hoặc HLS) | không |
| Proxy video | Worker / Pages Fn (/p/* — CORS/Range/confirm) |
edge-only |
| Watch-state | IndexedDB | không |
title (phim) có 1..N parts (tập). kind = movie (phim lẻ, ~1 part) | series (phim bộ, N part). Mỗi part trỏ 1 blob video qua token + (tuỳ chọn) phụ đề rời.
- video-mp4 (phim đơn giản): 1 file/tập;
<video>seek bằng HTTP Range native. Hostgd/src→ qua proxy giữ206. - video-hls (phim ABR): master playlist + segment immutable — mọi file tĩnh, CDN ăn khớp; ingest nặng hơn (transcode đa bitrate). Nên đặt trên R2/B2 phát thẳng.
- subtitles:
.vtt/.srtrời, đa ngôn ngữ, gắn<track>(host ngoài cần proxy cho CORS).
Điểm chung: index nhẹ ở R2 ⟂ blob immutable ở host khác. Range vào 1 file phục vụ "seek video"; host khó tính thì proxy chuẩn hoá Range/CORS.
- Payload lớn
*.v{rev}immutable → cache 1 năm. head.json= pointer ~50B, SWR 30s → cho biếtrevhiện tại.- Player:
head(tươi) →meta.v{rev}(cache mãi). Tập mới hiện ~30s. - Part list chunk hoá: chunk mới nhất versioned, chunk cũ immutable → thêm tập không ghi lại chunk cũ.
r2/b2 phát thẳng. gd/src qua /p/<token>: forward Range → giữ 206; gắn CORS; bóc Drive confirm-token; cache cứng từng byte-range; giấu origin. Đầy đủ ở docs/proxy.md. resolveBlob() trong contract.ts quyết định thẳng-hay-proxy ở client.
feed/search/genre/status entry là CatalogEntry tự chứa {title, cover, year, status, genres[], rating, latest} → list view 1 fetch là render. Detail view mới fetch record đầy đủ.
Mỗi facet = 1 listing tĩnh dựng sẵn (kiểu Hitomi nozomi). Vì entry mang đủ facet, client refine tại chỗ.
| Truy vấn | Cách |
|---|---|
| Theo tên | tải search/index.v{N} (CatalogEntry[]) → lọc substring/fuzzy in-memory |
| Theo thể loại | GET genre/{slug}/{page} |
| Theo tình trạng | GET status/{status}/{page} hoặc lọc client-side |
| Theo năm / kind | lọc client-side trên .year/.kind (filterEntries()) |
| Kết hợp AND | tải facet NHỎ NHẤT → lọc client-side trên .genres/.status/.year/.title |
| Sắp xếp | precompute biến thể (latest/popular/rating) hoặc sort client-side |
Catalog lớn: mỗi facet = nozomi id-list (sorted); client tải các id-list cần rồi INTERSECT; resolve id → CatalogEntry. Không product explosion, không server.
| Thành phần CHẾT | Đọc/Duyệt | Xem phim | Nội dung mới |
|---|---|---|---|
| GitHub Actions (ingest) | OK | OK | dừng tới khi hồi |
| R2 origin (blip) | OK (stale-if-error) |
OK (blob cache) | - |
| Drive (quota/ban) | OK | WARN: phim chưa cache lỗi | - |
| proxy | OK (data thẳng R2) | WARN: chỉ phim gd/src; r2/b2 vẫn xem được |
- |
| Cloudflare | DOWN (hiếm, SLA cao) | DOWN | - |
Không app server trên đường đọc → không SPOF cổ điển. Crawler & proxy thay/sập không kéo sập read path data.
SEO không cần prerender (Google render JS). Ưu tiên UI/UX player: seek nhanh, đổi chất lượng (HLS), chọn phụ đề, nhớ tiến độ (IndexedDB), dark mode, responsive. Bổ sung nhẹ <title>/OG + sitemap.xml (sinh lúc ingest) cho social card.
R2 10GB data free (egress $0) · Pages free · Worker proxy 100k/ngày free ($5/tháng = 10M). MVP read-only ≈ $0; chi phí tăng = storage video ở host bạn chọn.
Cập nhật trễ ~TTL pointer (30s) · không real-time · comment/social cần lớp dynamic · Drive = MVP (quota/ban), chuyển R2/B2 khi nghiêm túc.