KOReader plugin to browse and read books from Google Drive, offline-first
Cập nhật 2026-10-25 theo phản hồi: thiết bị Kobo; đã có OAuth client id/secret; thư viện = nhiều folder nguồn do user thêm (gồm cả folder được chia sẻ); hạn mức dung lượng do user cấu hình; không đồng bộ ngược tiến độ đọc (để dành cho kế hoạch sau).
Cập nhật lần 2: dùng chung OAuth client với rclone (Desktop); nguồn lớn nhất ~200–300 GB, nhiều loại file, cả truyện tranh lẫn truyện chữ; thông số đọc trực tiếp từ Kobo qua USB (mục 5.6).
Mục tiêu: KOReader trên máy đọc sách duyệt được các thư mục sách trên Google Drive, tải từng cuốn về khi cần đọc, và vẫn duyệt/đọc được khi Wi-Fi tắt.
Ràng buộc:
| Ràng buộc | Hệ quả thiết kế |
|---|---|
| Bộ nhớ thiết bị nhỏ | Chỉ tải sách khi cần, hạn mức dung lượng do user đặt, tự xoá bớt (LRU) |
| CPU/RAM yếu, không duyệt Drive trực tiếp được | Chỉ gọi REST API tối giản, gộp request, UI dạng danh sách văn bản, phân trang |
| Wi-Fi thường tắt | Offline-first: mọi thao tác duyệt chạy trên catalog cache cục bộ; mạng chỉ dùng khi user chủ động “Sync” |
| PDF chỉ đọc tốt bằng KOReader | Giải pháp nằm bên trong KOReader (plugin Lua), mở sách bằng reader của KOReader |
Luồng sử dụng:
Ngoài phạm vi (để sau): đồng bộ tiến độ đọc/highlight giữa nhiều thiết bị, backup lên Drive. Thiết kế hiện tại chỉ cần scope chỉ đọc; khi làm phần này sẽ cần xin thêm scope ghi.
gdrive.koplugin) dùng ssl.https/socket.http có sẵn trong KOReader.rclone serve webdav gdrive:Books; KOReader dùng Cloud storage → WebDAV có sẵn.https://www.googleapis.com/auth/drive.readonly. Scope này đọc được mọi file/folder user có quyền xem: My Drive, Shared with me, Shared drives.drive.file/drive.appdata, tức là chỉ thấy file do app tạo ra, không duyệt được thư viện có sẵn.http://127.0.0.1:<port> mà không phải đăng ký trước. Nếu client hiện có là Web application thì phải thêm redirect URI http://127.0.0.1:8765/ vào Console, hoặc tạo thêm một client Desktop (miễn phí, tạo nhanh).access_type=offline&prompt=consent để chắc chắn nhận được refresh token.| Cách | Mô tả | Đánh giá |
|---|---|---|
| A. Script trên PC | tools/gdrive_login.py (Python thuần, không cần thư viện ngoài): mở trình duyệt → đăng nhập → nhận code qua loopback → đổi lấy refresh token → ghi file gdrive_auth.lua. Chép vào Kobo qua USB. |
Chắc chắn chạy, làm được ngay. Dùng cho MVP. |
| B. Ghép đôi qua điện thoại trong mạng LAN (không cần PC) | Plugin mở một HTTP server nhỏ trên Kobo và hiện QR code trỏ tới http://<ip-kobo>:8765/. Điện thoại quét QR → trang trên Kobo có nút “Đăng nhập Google”. Sau khi đồng ý, Google redirect điện thoại tới http://127.0.0.1:8765/?code=…, trang này lỗi không tải được (đó là chuyện bình thường). User copy URL trên thanh địa chỉ, quay lại tab trang Kobo, dán vào ô và bấm gửi. Kobo tách lấy code và tự đổi lấy refresh token. |
Không cần PC, không phải gõ trên bàn phím e-ink; hơi nhiều bước nhưng chỉ làm một lần. Làm ở Phase 5. |
| C. Service account | Chia sẻ folder cho email của service account; Kobo tự ký JWT RS256. | Phải ký RSA trong Lua qua FFI OpenSSL → phức tạp; folder phải share riêng cho nó. Chỉ là dự phòng. |
| D. API key + folder “Anyone with link” | Không cần OAuth. | Sách bị public theo link. Chỉ dùng nếu chấp nhận điều đó. |
Đề xuất: A cho MVP, sau đó B để thiết bị tự đăng nhập lại khi token bị thu hồi.
Client hiện có là loại Desktop, đang dùng cho rclone → dùng lại được, và rclone thay luôn được script đăng nhập ở cách A:
# Tạo một remote riêng chỉ đọc cho Kobo. rclone tự mở trình duyệt để đăng nhập.
rclone config create kobo-gdrive drive client_id=<ID> client_secret=<SECRET> scope=drive.readonly
# Xem token vừa tạo (trong đó có refresh_token)
rclone config dump
Sau đó tools/rclone_to_kobo.py đọc rclone config dump và ghi ra gdrive_auth.lua (client_id, client_secret, refresh_token), có thể ghi thẳng vào E:\.adds\koreader\settings\ khi Kobo đang cắm USB.
Lưu ý:
drive.readonly, đừng lấy token của remote rclone đang dùng (thường có scope drive toàn quyền). Hai token độc lập nhau.https://oauth2.googleapis.com/revoke?token=<refresh_token> (plugin sẽ có nút “Đăng xuất” làm việc này).koreader/settings/gdrive_auth.lua: client_id, client_secret, refresh_token.POST https://oauth2.googleapis.com/token (grant_type=refresh_token) → access token (~1 giờ), giữ trong RAM; gặp 401 thì refresh; gặp invalid_grant thì báo “Cần đăng nhập lại” kèm hướng dẫn.Date của response, cảnh báo khi lệch lớn.User quản lý danh sách nguồn trong Google Drive → Nguồn sách. Mỗi nguồn có:
folder_id, tên hiển thị (đổi được), loại (my_drive / shared_with_me / shared_drive), drive_id nếu là shared drive.full: tải metadata của toàn bộ cây (mặc định cho folder vừa phải);depth N: chỉ N cấp, folder sâu hơn để trạng thái “chưa đồng bộ”;on_demand: chỉ đồng bộ một folder khi user mở nó lúc đang online (dành cho folder khổng lồ).Cách thêm nguồn (có nhiều cách vì gõ trên Kobo rất cực):
'root' in parentssharedWithMe = true and mimeType = 'application/vnd.google-apps.folder'drives.liststarred = true (lối tắt tiện cho folder hay dùng)Giữ lâu một folder → “Thêm làm nguồn”.
drive.google.com/drive/folders/<id>.gdrive_sources.lua qua USB (cho người thích tự làm).Chi phí chính trên Kobo là số lượng request HTTPS (mỗi request ~0,3–1 giây kể cả bắt tay TLS trên Wi-Fi yếu), không phải dung lượng dữ liệu. Các cách:
| # | Cách | Số request | Nhận xét |
|---|---|---|---|
| 1 | BFS ngây thơ: mỗi folder một lệnh files.list |
= số folder | 500 folder ≈ 5–8 phút. Quá chậm. |
| 2 | BFS gộp cha: q = ('id1' in parents or 'id2' in parents or … ) and trashed = false, gộp 20–40 folder mỗi request (giới hạn bởi độ dài URL), pageSize=1000 |
≈ số folder / 30 + số trang | Giảm 20–40 lần. Làm được cho mọi loại nguồn. Mặc định. |
| 3 | Quét cả shared drive một lần: corpora=drive&driveId=…&includeItemsFromAllDrives=true&supportsAllDrives=true, liệt kê mọi item rồi dựng cây cục bộ theo parents |
≈ số item / 1000 | Nhanh nhất với shared drive chủ yếu chứa sách. Nếu drive có nhiều file rác (ảnh…) thì lọc theo mimeType/đuôi file trong q. |
| 4 | Lấy hết folder trước, file sau: một truy vấn mimeType = folder cho toàn bộ My Drive → dựng cây → chỉ lấy file của các folder nằm trong nguồn (dùng cách 2) |
ít | Tốt khi My Drive có nhiều folder nhưng nguồn chỉ là một nhánh nhỏ. Tối ưu thêm, không bắt buộc. |
| 5 | Tăng dần bằng Changes API: changes.getStartPageToken sau lần quét đầy đủ, các lần sau changes.list (riêng cho My Drive/Shared-with-me và cho từng shared drive) |
thường 1–2 | Sync hằng ngày gần như tức thì. Mỗi thay đổi được đối chiếu với cây đã biết: item có cha nằm trong nguồn thì cập nhật, item bị chuyển ra ngoài hoặc xoá thì gỡ. |
| 6 | Lười theo folder (on_demand): mở folder khi online thì gọi files.list cho đúng folder đó và cache lại |
1 mỗi lần mở | Cho folder khổng lồ; offline chỉ thấy những gì đã từng mở. |
Lọc ngay trong truy vấn (quan trọng với nguồn 200–300 GB nhiều loại file): thêm vào q các điều kiện not mimeType contains 'video/' and not mimeType contains 'audio/'; ảnh (image/) cũng bị loại, trừ khi nguồn bật chế độ truyện tranh dạng folder ảnh (4.4). Drive không lọc được theo đuôi file trong q, nên phần còn lại lọc theo đuôi ngay trên thiết bị, và chỉ lưu vào SQLite những item là sách hoặc folder chứa sách. Nhờ vậy catalog nhỏ và lượng dữ liệu tải về giảm mạnh.
Ước lượng cho nguồn lớn nhất: giả sử khoảng 30.000 file sách và 3.000 folder:
Nếu thực tế lớn hơn nhiều thì đặt nguồn này ở chế độ depth 2 hoặc on_demand.
Chiến lược đề xuất: lần đầu quét đầy đủ bằng cách 2 (hoặc cách 3 cho shared drive); các lần sau dùng cách 5; folder nguồn đặt on_demand thì dùng cách 6. Có thêm nút “Quét lại toàn bộ” để sửa khi cache lệch.
Lưu ý: không dựa vào
modifiedTimecủa folder để phát hiện thay đổi, vì Drive không cập nhật trường này một cách tin cậy khi file con thay đổi. Changes API mới là nguồn đáng tin.
Các trường hợp đặc biệt của Drive:
application/vnd.google-apps.shortcut): lấy shortcutDetails(targetId, targetMimeType); shortcut trỏ tới folder thì duyệt tiếp vào target, có chống vòng lặp (tập folder đã thăm).file_id, các chỗ khác cùng trỏ tới.Trường dữ liệu yêu cầu từ API (giữ response nhỏ):
nextPageToken, files(id, name, mimeType, size, modifiedTime, md5Checksum, parents, shortcutDetails, trashed)
Màn hình gốc của plugin:
Google Drive Sync: 2 ngày trước
───────────────────────────────────────────────
🔍 Tìm kiếm
🕘 Đọc gần đây
🆕 Mới trên Drive (từ lần sync trước) (7)
💾 Trên máy 1,2/2 GB
⏳ Hàng đợi tải (3)
───────────────────────────────────────────────
📚 Sách giáo khoa (124 · 18 đã tải)
📚 Truyện (shared) (56 · 4 đã tải)
📚 Papers [on-demand]
(sách · đã tải), tính sẵn khi sync để mở folder không phải đếm lại.LIKE, chạy offline);A / B / C hiện thành một dòng (tuỳ chọn).✓ đã có trên máy · ☁ chưa tải · ⏳ đang chờ · ↻ trên Drive có bản mới hơn · 📌 đã ghim · ⚠ lỗi tải. Kèm kích thước file.ReaderUI:showReader(path); sách chưa tải → tải (nếu online) hoặc thêm vào hàng đợi.Menu widget có sẵn (danh sách văn bản, phân trang theo màn hình); không có thumbnail ở v1.on_demand chưa từng mở mà đang offline: hiện “Chưa đồng bộ – bật Wi-Fi để xem”.Nguồn có cả truyện chữ lẫn truyện tranh, nên phải xử lý các dạng sau:
| Dạng trên Drive | Cách xử lý |
|---|---|
| pdf, epub, mobi, azw3, fb2, djvu, txt… | Tải nguyên file, mở bằng KOReader |
| cbz / zip | Tải nguyên file; KOReader mở trực tiếp |
| cbr / rar, cb7 / 7z | Tải nguyên file. KOReader v2025.04 trên máy có libarchive (plugin Archive viewer), nhưng phải thử mở một file .cbr/.cb7 thật ở Phase 1. Nếu không đọc được như một cuốn truyện thì cho hiện kèm cảnh báo, hoặc (tuỳ chọn) chuyển thành .cbz ngay trên máy sau khi tải bằng ffi/archiver (giải nén) + ffi/zipwriter (đóng gói) |
| Folder chứa toàn ảnh (mỗi chapter là một folder jpg/png/webp) | Bật chế độ “Folder ảnh = truyện” cho từng nguồn (mặc định tắt). Khi đó folder có ≥ N ảnh và không có folder con hiện như một cuốn sách (📖 Chap 12 · 45 trang · 18 MB). Lúc tải: lấy lần lượt từng ảnh rồi đóng gói thành .cbz trên máy bằng ffi/zipwriter (chế độ store, không nén lại). Cần nhiều request nên chậm hơn, nhưng chạy trong hàng đợi và resume được theo từng ảnh |
| Ảnh lẻ, video, audio, Google Docs native, file khác | Bỏ qua |
Phần duyệt bổ sung cho truyện tranh:
Chap 2 trước Chap 10), quan trọng với tên chapter.Cách đã xem xét và loại bỏ: tạo file giả 0 byte trong File Manager của KOReader để duyệt bằng giao diện có sẵn. Loại vì KOReader sẽ cố mở hoặc đọc metadata các file đó, làm rối History/Collections, và không có chỗ hiển thị trạng thái. Thay vào đó, sách đã tải vẫn nằm trong thư mục thật nên File Manager, History, Collections của KOReader dùng bình thường.
gdrive.koplugin/
├── _meta.lua -- tên, mô tả plugin
├── main.lua -- đăng ký menu, Dispatcher actions, điều phối
├── auth.lua -- đọc gdrive_auth.lua, refresh access token
├── api.lua -- Drive API: files.list (gộp cha), drives.list, changes, download
├── catalog.lua -- SQLite: sources, items, edges, queue
├── sources.lua -- quản lý nguồn + folder picker online
├── sync.lua -- quét đầy đủ / tăng dần / on-demand, xử lý hàng đợi
├── downloader.lua -- tải ra .part, Range resume, kiểm tra size, tiến độ
├── storage.lua -- hạn mức, LRU eviction, ghim
├── browser.lua -- UI duyệt offline (Menu widget), tìm kiếm, danh sách phẳng
└── pairing.lua -- (Phase 5) HTTP server LAN: đăng nhập qua điện thoại, thêm nguồn
tools/
└── gdrive_login.py -- script trên PC sinh gdrive_auth.lua
File: koreader/settings/gdrive/catalog.sqlite3 (KOReader có sẵn lua-ljsqlite3).
sources(id PK, folder_id, name, kind, drive_id, sync_mode, depth,
enabled, status, changes_token, last_sync)
items(id PK, -- Drive file id
name, mime_type, is_folder, size, modified_time, md5,
shortcut_target,
book_count, local_count, -- chỉ cho folder, tính sẵn khi sync
synced, -- folder on_demand/depth: đã liệt kê con chưa
local_path, local_modified_time,
state, -- remote|queued|downloading|local|outdated|error
pinned, last_opened, first_seen)
edges(parent_id, child_id, PRIMARY KEY(parent_id, child_id)) -- nhiều-nhiều
meta(key PK, value) -- last_sync, schema_version, ...
edges(parent_id), items(name COLLATE NOCASE), items(state), items(last_opened).book_count/local_count theo từng nhánh sau khi sync.GET /drive/v3/files/{id}?alt=media&supportsAllDrives=true, ghi thẳng ra file bằng ltn12.sink.file (không giữ file trong RAM, quan trọng với PDF lớn).*.part, xong mới rename. Wi-Fi rớt thì resume bằng Range: bytes=<size .part>-.size khớp metadata; MD5 để tuỳ chọn (chậm trên máy yếu với file lớn).<home>/.gdrive/<tên nguồn>/<đường dẫn như trên Drive>/ (xem 5.6 về lý do dùng thư mục bắt đầu bằng dấu chấm trên Kobo). File có nhiều đường dẫn thì lưu theo đường dẫn đầu tiên.last_opened cũ nhất), bỏ qua sách đã ghim và sách đang mở; trước khi xoá hiện danh sách sẽ bị xoá để user xác nhận (có tuỳ chọn “tự động, không hỏi”).remote, vẫn hiện trong catalog và tải lại được.document_metadata_folder = "doc", tức là .sdr nằm cạnh file sách, nên xoá sách là mất tiến độ. Cần đổi Settings → Document → Book metadata location sang “hash” (hoặc “koreader/docsettings”). Khi đổi, KOReader sẽ hỏi có chuyển metadata hiện có sang chỗ mới không: chọn có. Khi khởi động, plugin cảnh báo nếu còn để “doc”.NetworkMgr:isOnline() để quyết định tải ngay hay xếp hàng.NetworkMgr:runWhenOnline(cb) (KOReader sẽ hỏi bật Wi-Fi).NetworkConnected, mặc định tắt).UIManager:scheduleIn để UI không bị treo; sync bị ngắt giữa chừng thì lần sau chạy tiếp (trạng thái nằm trong SQLite).Thông số đọc từ máy (ổ E:, 2026-10-25):
| Mục | Giá trị | Hệ quả |
|---|---|---|
| KOReader | v2025.04 (.adds/koreader/git-rev) |
Phiên bản mục tiêu để phát triển và test |
| Firmware Kobo | 4.38.23429 | |
| Bộ nhớ | 6,9 GB, chỉ còn trống 39 MB | Phải dọn chỗ trước khi test (Phase 0.5) |
| Sách hiện có | /mnt/onboard/Home = 6,3 GB (home_dir của KOReader) |
Ứng viên chuyển lên Drive |
| Dữ liệu KOReader | ota/ 91 MB (gói cập nhật cũ), cache/ 47 MB |
Xoá được, lấy lại ~140 MB |
| Nickel | KoboReader.sqlite 139 MB; ExcludeSyncFolders đã bỏ qua thư mục bắt đầu bằng dấu chấm |
Đặt thư mục tải là .gdrive thì Nickel không quét |
| Metadata sách | document_metadata_folder = "doc" |
Đổi sang “hash” (5.4) |
| Thư viện có sẵn | libsqlite3, libarchive, libssl/libcrypto, ffi/zipwriter.lua, ffi/archiver.lua |
Đủ cho catalog, đóng gói .cbz, HTTPS |
| Plugin có sẵn | SSH.koplugin, httpinspector, qrclipboard |
SSH dùng để phát triển; QR dùng cho ghép đôi qua điện thoại |
Các lưu ý khác:
/mnt/onboard/.gdrive/. Thư mục Home hiện tại không có dấu chấm nên Nickel vẫn quét nó (một phần lý do KoboReader.sqlite lớn)..gdrive làm lối tắt trong File Manager của KOReader để dễ vào./mnt/onboard, nên được tính vào hạn mức.| Phase | Nội dung | Kết quả |
|---|---|---|
| 0 (tuỳ chọn) | rclone serve webdav + Cloud storage WebDAV của KOReader |
Đọc được ngay trong lúc chờ plugin |
| 0.5. Dọn chỗ | Xoá ota/ và cache/ của KOReader; đổi metadata sang “hash”; sao lưu Home lên Drive rồi xoá bớt sách ít đọc |
Còn trống tối thiểu ~500 MB để test |
| 1. Spike | Tạo token bằng rclone + rclone_to_kobo.py; trên Kobo: refresh token, list 1 folder, tải 1 file, kiểm tra TLS và đồng hồ; mở thử .cbz/.cbr/.cb7 |
Chứng minh OAuth và HTTPS chạy trên Kobo thật; biết định dạng truyện nào đọc được |
| 2. Nguồn + catalog | SQLite schema, folder picker online (My Drive / Shared with me / Shared drives / Starred), quét đầy đủ kiểu BFS gộp cha, shortcut, lọc định dạng | Thêm được nhiều nguồn, có cache cây thư mục |
| 3. Duyệt offline + tải | browser.lua (cây + Tìm kiếm + Trên máy + Hàng đợi), downloader (.part, resume), hàng đợi offline |
MVP: luồng sử dụng chính chạy được |
| 4. Dung lượng | Hạn mức, LRU, ghim, cảnh báo docsettings, màn hình Trên máy | An toàn với bộ nhớ nhỏ |
| 5. Hoàn thiện | Changes API (sync tăng dần), chế độ on_demand/depth, Mới trên Drive, ghép đôi qua điện thoại (đăng nhập + thêm nguồn qua LAN), tự tắt Wi-Fi, Dispatcher, đổi tên/chuyển file theo Drive |
Dùng hằng ngày thoải mái |
| 6. Truyện tranh | Chế độ “Folder ảnh = truyện” + đóng gói .cbz, “Tải N chương tiếp theo”, chuyển cbr/cb7 sang cbz nếu cần, xoá chương đã đọc | Đọc truyện tranh dạng folder ảnh |
| Sau này | Đồng bộ tiến độ đọc/highlight nhiều thiết bị, backup lên Drive (cần scope ghi) | Kế hoạch riêng |
SSH.koplugin, chạy dropbear, có cả scp/sftp-server), kết nối qua Wi-Fi.scp -P 2222 -r gdrive.koplugin root@<ip-kobo>:/mnt/onboard/.adds/koreader/plugins/ssh -p 2222 root@<ip-kobo> tail -f /mnt/onboard/.adds/koreader/crash.log để xem log trực tiếp.E:\.adds\koreader\plugins\, eject rồi test. Chậm hơn vì mỗi lần cắm USB máy phải thoát khỏi KOReader.plugins/gdrive.koplugin. Cuối cùng vẫn phải test trên Kobo thật (mạng, hiệu năng).parents, chống vòng lặp shortcut, LRU, tạo câu q gộp cha) bằng busted, dùng response Drive mẫu.| Rủi ro | Giảm thiểu |
|---|---|
| Refresh token hết hạn sau 7 ngày | Consent screen để In production; báo lỗi rõ và đăng nhập lại được ngay trên Kobo (Phase 5) |
| Client id hiện có là loại Web, không redirect được về loopback | Thêm redirect URI loopback vào client, hoặc tạo client Desktop mới |
Google siết scope drive.readonly với app chưa verify |
Dự phòng: service account hoặc API key + folder chia sẻ theo link |
| Sync chậm với cây lớn | Gộp cha, quét cả shared drive một lần, Changes API, chế độ on_demand |
| Thiết bị treo khi sync | Transaction SQLite, yield UI giữa các trang, sync bị ngắt thì chạy tiếp được |
| Wi-Fi rớt giữa chừng | .part + Range resume, retry/backoff |
| Mất tiến độ đọc khi sách bị xoá/đổi tên | Docsettings theo hash (5.4) |
| Nickel quét trùng sách | Thư mục .gdrive (5.6) |
| API KOReader thay đổi giữa các phiên bản | Ghi rõ phiên bản KOReader tối thiểu đã test |
Đã chốt
drive.readonly bằng rclone config create; sau đó thêm ghép đôi qua điện thoại.Còn mở
rclone size gdrive:<folder> trên PC).Home (6,3 GB) có thể chuyển lên Drive để giải phóng chỗ?