feat(koplugin): bulk download all cloud books from Library view (#4751) (#4765)

Downloading a Readest cloud library into KOReader previously required
tapping each book one at a time. Add a "Download all books" action to
the Library view menu that pulls every cloud-only book to the device
in one pass.

- LibraryStore:listCloudOnlyBooks() returns the downloadable
  cloud-present, not-local books for the current user (whole library,
  independent of the active search/group view).
- librarywidget.downloadAll() streams them sequentially via the
  existing syncbooks.downloadBook path inside a Trapper coroutine:
  cancellable progress (Trapper:info Abort/Continue), per-book
  failures skipped and counted, summary toast at the end. Bridges
  downloadBook's sync-or-async callback with a coroutine suspended
  check so it serializes correctly either way.
- Wire the action into the view-menu Actions section.
- Add the new UI strings and translate them across all 33 locales.

Closes #4751

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-24 23:42:31 +08:00
committed by GitHub
parent 163487b5e3
commit e0b537bc16
37 changed files with 804 additions and 0 deletions
@@ -406,6 +406,38 @@ function M:listBooks(filters)
return rows
end
-- ---------------------------------------------------------------------------
-- listCloudOnlyBooks — the bulk-download candidate set (#4751)
-- ---------------------------------------------------------------------------
-- Books that are in the cloud with a downloadable file but not yet on this
-- device: cloud_present = 1, local_present = 0, not deleted, and with an
-- uploaded_at (a phantom record without an uploaded file is unreachable, so
-- it's excluded just like listBooks excludes it). Returns full rows so the
-- caller can hand each straight to syncbooks.downloadBook. Ordered newest
-- first for a sensible progress sequence; hash ASC tiebreak for determinism.
function M:listCloudOnlyBooks()
local sql = string.format([[
SELECT %s FROM books
WHERE user_id = ?
AND deleted_at IS NULL
AND cloud_present = 1
AND local_present = 0
AND uploaded_at IS NOT NULL
ORDER BY COALESCE(updated_at, created_at) DESC, hash ASC
]], table.concat(BOOK_COLS, ", "))
local stmt = self.db:prepare(sql)
stmt:reset()
stmt:bind1(1, self.user_id)
local rows = {}
while true do
local r = stmt:step()
if not r then break end
rows[#rows + 1] = row_to_table(r)
end
stmt:close()
return rows
end
-- ---------------------------------------------------------------------------
-- getGroups
-- ---------------------------------------------------------------------------
@@ -114,6 +114,14 @@ function M.show(opts)
-- Actions
{ { text = _("Actions"), enabled = false } },
{
{
text = _("Download all books"),
callback = function()
require("library.librarywidget").downloadAll()
end,
},
},
{
{
text = _("Rescan library"),
@@ -19,6 +19,7 @@ local Trapper = require("ui/trapper")
local UIManager = require("ui/uimanager")
local logger = require("logger")
local _ = require("readest_i18n")
local T = require("ffi/util").template
local LibraryStore = require("library.librarystore")
local libraryitem = require("library.libraryitem")
@@ -948,6 +949,108 @@ local function removeLocalFile(row)
return true
end
-- ---------------------------------------------------------------------------
-- downloadAll() — bulk-download every cloud-only book to this device (#4751).
-- ---------------------------------------------------------------------------
-- The KOReader counterpart to "download all" on Readest web/desktop: gather
-- every book that's in the cloud with an uploaded file but not yet on this
-- device (LibraryStore:listCloudOnlyBooks) and stream them one at a time,
-- reusing the proven syncbooks.downloadBook path. Driven from the view-menu
-- Actions section; uses M._opts/M._store like M.refresh() so the caller
-- needs no arguments.
--
-- Progress + cancel: a Trapper:info message shows "Downloading X of N"; the
-- batch yields to the event loop between books, so tapping the message
-- raises Trapper's Abort/Continue confirm. Aborting finishes the in-flight
-- book and then halts. Per-book failures (404, network, unknown format) are
-- counted and skipped, never fatal, and a summary is shown at the end.
function M.downloadAll()
local opts = M._opts
if not opts or not M._store then return end
-- Same download-dir resolution + guard as the single-book tap path.
local download_dir = opts.settings.library_download_dir
or G_reader_settings:readSetting("home_dir")
if not download_dir or download_dir == "" then
UIManager:show(InfoMessage:new{
text = _("Set Home folder in File Manager first to enable downloads."),
timeout = 3,
})
return
end
local books = M._store:listCloudOnlyBooks()
local total = #books
if total == 0 then
UIManager:show(InfoMessage:new{
text = _("No books to download."), timeout = 3,
})
return
end
Trapper:wrap(function()
local co = coroutine.running()
local done, failed, cancelled = 0, 0, false
for i, book in ipairs(books) do
-- Trapper:info returns false when the user taps the message and
-- confirms Abort. It also yields to UIManager, which is what
-- lets a tap queued during the previous (blocking) download get
-- processed here, at the book boundary.
local go_on = Trapper:info(
T(_("Downloading %1 of %2…"), i, total) .. "\n" .. (book.title or ""))
if not go_on then
cancelled = true
break
end
-- Await the download. downloadBook fires its callback exactly
-- once, either synchronously (token already fresh) or after an
-- async token refresh. Only resume when the coroutine actually
-- suspended; if the callback already ran inline, skip the yield.
local finished, result = false, nil
syncbooks.downloadBook(book, {
sync_auth = opts.sync_auth,
sync_path = opts.sync_path,
settings = opts.settings,
download_dir = download_dir,
}, function(success, dst_or_err, status)
result = { success = success, dst = dst_or_err, status = status }
finished = true
if coroutine.status(co) == "suspended" then
coroutine.resume(co)
end
end)
if not finished then coroutine.yield() end
if result and result.success then
M._store:upsertBook({
hash = book.hash, title = book.title,
local_present = 1, file_path = result.dst,
})
done = done + 1
else
failed = failed + 1
end
end
-- On the cancel path Trapper:info already closed its widget when it
-- returned false; only the run-to-completion path leaves one showing.
if not cancelled then Trapper:clear() end
M.refresh()
local summary
if cancelled then
summary = T(_("Download cancelled. %1 of %2 downloaded."), done, total)
elseif failed > 0 then
summary = T(_("Downloaded %1 of %2 (skipped %3)."), done, total, failed)
else
summary = T(_("Downloaded %1 of %2."), done, total)
end
UIManager:show(InfoMessage:new{ text = summary, timeout = 3 })
end)
end
-- ---------------------------------------------------------------------------
-- handleHold(item, opts) — long-press action sheet
-- ---------------------------------------------------------------------------
@@ -68,6 +68,9 @@ msgstr "تصاعدي"
msgid "Actions"
msgstr "إجراءات"
msgid "Download all books"
msgstr "تنزيل كل الكتب"
msgid "Rescan library"
msgstr "إعادة فحص المكتبة"
@@ -134,6 +137,21 @@ msgstr "فشل تنزيل الغلاف."
msgid "Could not delete the file."
msgstr "تعذّر حذف الملف."
msgid "No books to download."
msgstr "لا توجد كتب للتنزيل."
msgid "Downloading %1 of %2…"
msgstr "جارٍ تنزيل %1 من %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "أُلغي التنزيل. تم تنزيل %1 من %2."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "تم تنزيل %1 من %2 (تم تخطّي %3)."
msgid "Downloaded %1 of %2."
msgstr "تم تنزيل %1 من %2."
msgid "Removing from cloud…"
msgstr "جارٍ الإزالة من السحابة…"
@@ -68,6 +68,9 @@ msgstr "আরোহী"
msgid "Actions"
msgstr "ক্রিয়াসমূহ"
msgid "Download all books"
msgstr "সব বই ডাউনলোড করুন"
msgid "Rescan library"
msgstr "লাইব্রেরি পুনরায় স্ক্যান করুন"
@@ -134,6 +137,21 @@ msgstr "প্রচ্ছদ ডাউনলোড ব্যর্থ।"
msgid "Could not delete the file."
msgstr "ফাইলটি মুছে ফেলা যায়নি।"
msgid "No books to download."
msgstr "ডাউনলোড করার মতো কোনো বই নেই।"
msgid "Downloading %1 of %2…"
msgstr "%2-এর মধ্যে %1 ডাউনলোড হচ্ছে…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "ডাউনলোড বাতিল করা হয়েছে। %2-এর মধ্যে %1 ডাউনলোড হয়েছে।"
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%2-এর মধ্যে %1 ডাউনলোড হয়েছে (%3 বাদ দেওয়া হয়েছে)।"
msgid "Downloaded %1 of %2."
msgstr "%2-এর মধ্যে %1 ডাউনলোড হয়েছে।"
msgid "Removing from cloud…"
msgstr "ক্লাউড থেকে সরানো হচ্ছে…"
@@ -68,6 +68,9 @@ msgstr "ཡར་འཛེག་"
msgid "Actions"
msgstr "བྱ་བ་"
msgid "Download all books"
msgstr "དེབ་ཡོངས་ཕབ་ལེན་"
msgid "Rescan library"
msgstr "དཔེ་མཛོད་ཡང་བསྐྱར་ཞིབ་ཤིབ་"
@@ -134,6 +137,21 @@ msgstr "འགྲམ་ཤོག་ཕབ་ལེན་མ་ཐུབ།"
msgid "Could not delete the file."
msgstr "ཡིག་ཆ་སུབ་མ་ཐུབ།"
msgid "No books to download."
msgstr "ཕབ་ལེན་བྱ་རྒྱུའི་དེབ་མེད།"
msgid "Downloading %1 of %2…"
msgstr "%2 ནང་གི་ %1 ཕབ་ལེན་བྱེད་བཞིན་པ་…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "ཕབ་ལེན་འདོར་སོང་། %2 ནང་གི་ %1 ཕབ་ལེན་ཟིན།"
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%2 ནང་གི་ %1 ཕབ་ལེན་ཟིན། (%3 མཆོངས་སོང་།)"
msgid "Downloaded %1 of %2."
msgstr "%2 ནང་གི་ %1 ཕབ་ལེན་ཟིན།"
msgid "Removing from cloud…"
msgstr "སྤྲིན་གྲུབ་ནས་སུབ་བཞིན་པ་…"
@@ -68,6 +68,9 @@ msgstr "Aufsteigend"
msgid "Actions"
msgstr "Aktionen"
msgid "Download all books"
msgstr "Alle Bücher herunterladen"
msgid "Rescan library"
msgstr "Bibliothek erneut einlesen"
@@ -134,6 +137,21 @@ msgstr "Cover-Download fehlgeschlagen."
msgid "Could not delete the file."
msgstr "Datei konnte nicht gelöscht werden."
msgid "No books to download."
msgstr "Keine Bücher zum Herunterladen."
msgid "Downloading %1 of %2…"
msgstr "%1 von %2 wird heruntergeladen…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Download abgebrochen. %1 von %2 heruntergeladen."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 von %2 heruntergeladen (%3 übersprungen)."
msgid "Downloaded %1 of %2."
msgstr "%1 von %2 heruntergeladen."
msgid "Removing from cloud…"
msgstr "Wird aus der Cloud entfernt …"
@@ -68,6 +68,9 @@ msgstr "Αύξουσα"
msgid "Actions"
msgstr "Ενέργειες"
msgid "Download all books"
msgstr "Λήψη όλων των βιβλίων"
msgid "Rescan library"
msgstr "Επανασάρωση βιβλιοθήκης"
@@ -134,6 +137,21 @@ msgstr "Αποτυχία λήψης εξωφύλλου."
msgid "Could not delete the file."
msgstr "Δεν ήταν δυνατή η διαγραφή του αρχείου."
msgid "No books to download."
msgstr "Δεν υπάρχουν βιβλία για λήψη."
msgid "Downloading %1 of %2…"
msgstr "Γίνεται λήψη %1 από %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Η λήψη ακυρώθηκε. Έγινε λήψη %1 από %2."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "Έγινε λήψη %1 από %2 (παραλείφθηκαν %3)."
msgid "Downloaded %1 of %2."
msgstr "Έγινε λήψη %1 από %2."
msgid "Removing from cloud…"
msgstr "Αφαίρεση από το cloud…"
@@ -68,6 +68,9 @@ msgstr "Ascendente"
msgid "Actions"
msgstr "Acciones"
msgid "Download all books"
msgstr "Descargar todos los libros"
msgid "Rescan library"
msgstr "Reescanear biblioteca"
@@ -134,6 +137,21 @@ msgstr "Falló la descarga de la portada."
msgid "Could not delete the file."
msgstr "No se pudo eliminar el archivo."
msgid "No books to download."
msgstr "No hay libros para descargar."
msgid "Downloading %1 of %2…"
msgstr "Descargando %1 de %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Descarga cancelada. %1 de %2 descargados."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 de %2 descargados (%3 omitidos)."
msgid "Downloaded %1 of %2."
msgstr "%1 de %2 descargados."
msgid "Removing from cloud…"
msgstr "Eliminando de la nube…"
@@ -68,6 +68,9 @@ msgstr "صعودی"
msgid "Actions"
msgstr "اقدامات"
msgid "Download all books"
msgstr "دانلود همه کتاب‌ها"
msgid "Rescan library"
msgstr "اسکن مجدد کتابخانه"
@@ -134,6 +137,21 @@ msgstr "دانلود جلد ناموفق بود."
msgid "Could not delete the file."
msgstr "حذف فایل ممکن نشد."
msgid "No books to download."
msgstr "کتابی برای دانلود وجود ندارد."
msgid "Downloading %1 of %2…"
msgstr "در حال دانلود %1 از %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "دانلود لغو شد. %1 از %2 دانلود شد."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 از %2 دانلود شد (%3 رد شد)."
msgid "Downloaded %1 of %2."
msgstr "%1 از %2 دانلود شد."
msgid "Removing from cloud…"
msgstr "در حال حذف از فضای ابری…"
@@ -68,6 +68,9 @@ msgstr "Croissant"
msgid "Actions"
msgstr "Actions"
msgid "Download all books"
msgstr "Télécharger tous les livres"
msgid "Rescan library"
msgstr "Réanalyser la bibliothèque"
@@ -134,6 +137,21 @@ msgstr "Échec du téléchargement de la couverture."
msgid "Could not delete the file."
msgstr "Impossible de supprimer le fichier."
msgid "No books to download."
msgstr "Aucun livre à télécharger."
msgid "Downloading %1 of %2…"
msgstr "Téléchargement de %1 sur %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Téléchargement annulé. %1 sur %2 téléchargés."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 sur %2 téléchargés (%3 ignorés)."
msgid "Downloaded %1 of %2."
msgstr "%1 sur %2 téléchargés."
msgid "Removing from cloud…"
msgstr "Suppression du cloud…"
@@ -68,6 +68,9 @@ msgstr "עולה"
msgid "Actions"
msgstr "פעולות"
msgid "Download all books"
msgstr "הורד את כל הספרים"
msgid "Rescan library"
msgstr "סרוק ספרייה מחדש"
@@ -134,6 +137,21 @@ msgstr "הורדת הכריכה נכשלה."
msgid "Could not delete the file."
msgstr "לא ניתן למחוק את הקובץ."
msgid "No books to download."
msgstr "אין ספרים להורדה."
msgid "Downloading %1 of %2…"
msgstr "מוריד %1 מתוך %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "ההורדה בוטלה. %1 מתוך %2 הורדו."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 מתוך %2 הורדו (%3 דולגו)."
msgid "Downloaded %1 of %2."
msgstr "%1 מתוך %2 הורדו."
msgid "Removing from cloud…"
msgstr "מוסר מהענן…"
@@ -68,6 +68,9 @@ msgstr "आरोही"
msgid "Actions"
msgstr "क्रियाएँ"
msgid "Download all books"
msgstr "सभी पुस्तकें डाउनलोड करें"
msgid "Rescan library"
msgstr "लाइब्रेरी पुनः स्कैन करें"
@@ -134,6 +137,21 @@ msgstr "कवर डाउनलोड विफल।"
msgid "Could not delete the file."
msgstr "फ़ाइल हटाई नहीं जा सकी।"
msgid "No books to download."
msgstr "डाउनलोड करने के लिए कोई पुस्तक नहीं है।"
msgid "Downloading %1 of %2…"
msgstr "%2 में से %1 डाउनलोड हो रहा है…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "डाउनलोड रद्द किया गया। %2 में से %1 डाउनलोड हुई।"
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%2 में से %1 डाउनलोड हुई (%3 छोड़ी गईं)।"
msgid "Downloaded %1 of %2."
msgstr "%2 में से %1 डाउनलोड हुई।"
msgid "Removing from cloud…"
msgstr "क्लाउड से हटाया जा रहा है…"
@@ -68,6 +68,9 @@ msgstr "Növekvő"
msgid "Actions"
msgstr "Műveletek"
msgid "Download all books"
msgstr "Összes könyv letöltése"
msgid "Rescan library"
msgstr "Könyvtár újraolvasása"
@@ -134,6 +137,21 @@ msgstr "A borító letöltése sikertelen."
msgid "Could not delete the file."
msgstr "A fájl nem törölhető."
msgid "No books to download."
msgstr "Nincs letölthető könyv."
msgid "Downloading %1 of %2…"
msgstr "Letöltés: %1 / %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Letöltés megszakítva. %2 könyvből %1 letöltve."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%2 könyvből %1 letöltve (%3 kihagyva)."
msgid "Downloaded %1 of %2."
msgstr "%2 könyvből %1 letöltve."
msgid "Removing from cloud…"
msgstr "Eltávolítás a felhőből…"
@@ -68,6 +68,9 @@ msgstr "Naik"
msgid "Actions"
msgstr "Tindakan"
msgid "Download all books"
msgstr "Unduh semua buku"
msgid "Rescan library"
msgstr "Pindai ulang pustaka"
@@ -134,6 +137,21 @@ msgstr "Unduhan sampul gagal."
msgid "Could not delete the file."
msgstr "Tidak dapat menghapus berkas."
msgid "No books to download."
msgstr "Tidak ada buku untuk diunduh."
msgid "Downloading %1 of %2…"
msgstr "Mengunduh %1 dari %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Unduhan dibatalkan. %1 dari %2 terunduh."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 dari %2 terunduh (%3 dilewati)."
msgid "Downloaded %1 of %2."
msgstr "%1 dari %2 terunduh."
msgid "Removing from cloud…"
msgstr "Menghapus dari cloud…"
@@ -68,6 +68,9 @@ msgstr "Crescente"
msgid "Actions"
msgstr "Azioni"
msgid "Download all books"
msgstr "Scarica tutti i libri"
msgid "Rescan library"
msgstr "Riscansiona libreria"
@@ -134,6 +137,21 @@ msgstr "Download della copertina non riuscito."
msgid "Could not delete the file."
msgstr "Impossibile eliminare il file."
msgid "No books to download."
msgstr "Nessun libro da scaricare."
msgid "Downloading %1 of %2…"
msgstr "Download di %1 di %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Download annullato. %1 di %2 scaricati."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 di %2 scaricati (%3 saltati)."
msgid "Downloaded %1 of %2."
msgstr "%1 di %2 scaricati."
msgid "Removing from cloud…"
msgstr "Rimozione dal cloud…"
@@ -68,6 +68,9 @@ msgstr "昇順"
msgid "Actions"
msgstr "操作"
msgid "Download all books"
msgstr "すべての書籍をダウンロード"
msgid "Rescan library"
msgstr "ライブラリを再スキャン"
@@ -134,6 +137,21 @@ msgstr "表紙のダウンロードに失敗しました。"
msgid "Could not delete the file."
msgstr "ファイルを削除できませんでした。"
msgid "No books to download."
msgstr "ダウンロードする書籍がありません。"
msgid "Downloading %1 of %2…"
msgstr "%2 件中 %1 件目をダウンロード中…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "ダウンロードをキャンセルしました。%2 件中 %1 件をダウンロードしました。"
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%2 件中 %1 件をダウンロードしました(%3 件スキップ)。"
msgid "Downloaded %1 of %2."
msgstr "%2 件中 %1 件をダウンロードしました。"
msgid "Removing from cloud…"
msgstr "クラウドから削除中…"
@@ -68,6 +68,9 @@ msgstr "오름차순"
msgid "Actions"
msgstr "작업"
msgid "Download all books"
msgstr "모든 책 다운로드"
msgid "Rescan library"
msgstr "라이브러리 다시 검색"
@@ -134,6 +137,21 @@ msgstr "표지 다운로드에 실패했습니다."
msgid "Could not delete the file."
msgstr "파일을 삭제할 수 없습니다."
msgid "No books to download."
msgstr "다운로드할 책이 없습니다."
msgid "Downloading %1 of %2…"
msgstr "%2개 중 %1개 다운로드 중…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "다운로드가 취소되었습니다. %2개 중 %1개를 다운로드했습니다."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%2개 중 %1개를 다운로드했습니다 (%3개 건너뜀)."
msgid "Downloaded %1 of %2."
msgstr "%2개 중 %1개를 다운로드했습니다."
msgid "Removing from cloud…"
msgstr "클라우드에서 제거 중…"
@@ -68,6 +68,9 @@ msgstr "Menaik"
msgid "Actions"
msgstr "Tindakan"
msgid "Download all books"
msgstr "Muat turun semua buku"
msgid "Rescan library"
msgstr "Imbas semula pustaka"
@@ -134,6 +137,21 @@ msgstr "Muat turun kulit gagal."
msgid "Could not delete the file."
msgstr "Tidak dapat memadam fail."
msgid "No books to download."
msgstr "Tiada buku untuk dimuat turun."
msgid "Downloading %1 of %2…"
msgstr "Memuat turun %1 daripada %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Muat turun dibatalkan. %1 daripada %2 dimuat turun."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 daripada %2 dimuat turun (%3 dilangkau)."
msgid "Downloaded %1 of %2."
msgstr "%1 daripada %2 dimuat turun."
msgid "Removing from cloud…"
msgstr "Mengeluarkan daripada awan…"
@@ -68,6 +68,9 @@ msgstr "Oplopend"
msgid "Actions"
msgstr "Acties"
msgid "Download all books"
msgstr "Alle boeken downloaden"
msgid "Rescan library"
msgstr "Bibliotheek opnieuw scannen"
@@ -134,6 +137,21 @@ msgstr "Omslag downloaden mislukt."
msgid "Could not delete the file."
msgstr "Kon het bestand niet verwijderen."
msgid "No books to download."
msgstr "Geen boeken om te downloaden."
msgid "Downloading %1 of %2…"
msgstr "%1 van %2 wordt gedownload…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Download geannuleerd. %1 van %2 gedownload."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 van %2 gedownload (%3 overgeslagen)."
msgid "Downloaded %1 of %2."
msgstr "%1 van %2 gedownload."
msgid "Removing from cloud…"
msgstr "Verwijderen uit cloud …"
@@ -68,6 +68,9 @@ msgstr "Rosnąco"
msgid "Actions"
msgstr "Akcje"
msgid "Download all books"
msgstr "Pobierz wszystkie książki"
msgid "Rescan library"
msgstr "Skanuj bibliotekę ponownie"
@@ -134,6 +137,21 @@ msgstr "Nie udało się pobrać okładki."
msgid "Could not delete the file."
msgstr "Nie można usunąć pliku."
msgid "No books to download."
msgstr "Brak książek do pobrania."
msgid "Downloading %1 of %2…"
msgstr "Pobieranie %1 z %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Pobieranie anulowane. Pobrano %1 z %2."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "Pobrano %1 z %2 (pominięto %3)."
msgid "Downloaded %1 of %2."
msgstr "Pobrano %1 z %2."
msgid "Removing from cloud…"
msgstr "Usuwanie z chmury…"
@@ -68,6 +68,9 @@ msgstr "Crescente"
msgid "Actions"
msgstr "Ações"
msgid "Download all books"
msgstr "Baixar todos os livros"
msgid "Rescan library"
msgstr "Reescanear biblioteca"
@@ -134,6 +137,21 @@ msgstr "Falha ao baixar a capa."
msgid "Could not delete the file."
msgstr "Não foi possível excluir o arquivo."
msgid "No books to download."
msgstr "Nenhum livro para baixar."
msgid "Downloading %1 of %2…"
msgstr "Baixando %1 de %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Download cancelado. %1 de %2 baixados."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 de %2 baixados (%3 ignorados)."
msgid "Downloaded %1 of %2."
msgstr "%1 de %2 baixados."
msgid "Removing from cloud…"
msgstr "Removendo da nuvem…"
@@ -68,6 +68,9 @@ msgstr "Crescente"
msgid "Actions"
msgstr "Ações"
msgid "Download all books"
msgstr "Baixar todos os livros"
msgid "Rescan library"
msgstr "Reescanear biblioteca"
@@ -134,6 +137,21 @@ msgstr "Falha ao baixar a capa."
msgid "Could not delete the file."
msgstr "Não foi possível excluir o arquivo."
msgid "No books to download."
msgstr "Nenhum livro para baixar."
msgid "Downloading %1 of %2…"
msgstr "A baixar %1 de %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Download cancelado. %1 de %2 baixados."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%1 de %2 baixados (%3 ignorados)."
msgid "Downloaded %1 of %2."
msgstr "%1 de %2 baixados."
msgid "Removing from cloud…"
msgstr "Removendo da nuvem…"
@@ -68,6 +68,9 @@ msgstr "Crescător"
msgid "Actions"
msgstr "Acțiuni"
msgid "Download all books"
msgstr "Descarcă toate cărțile"
msgid "Rescan library"
msgstr "Rescanează biblioteca"
@@ -134,6 +137,21 @@ msgstr "Descărcarea copertei a eșuat."
msgid "Could not delete the file."
msgstr "Nu s-a putut șterge fișierul."
msgid "No books to download."
msgstr "Nicio carte de descărcat."
msgid "Downloading %1 of %2…"
msgstr "Se descarcă %1 din %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Descărcare anulată. %1 din %2 descărcate."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "Descărcate %1 din %2 (omise %3)."
msgid "Downloaded %1 of %2."
msgstr "Descărcate %1 din %2."
msgid "Removing from cloud…"
msgstr "Se elimină din cloud…"
@@ -68,6 +68,9 @@ msgstr "По возрастанию"
msgid "Actions"
msgstr "Действия"
msgid "Download all books"
msgstr "Загрузить все книги"
msgid "Rescan library"
msgstr "Пересканировать библиотеку"
@@ -134,6 +137,21 @@ msgstr "Не удалось загрузить обложку."
msgid "Could not delete the file."
msgstr "Не удалось удалить файл."
msgid "No books to download."
msgstr "Нет книг для загрузки."
msgid "Downloading %1 of %2…"
msgstr "Загрузка %1 из %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Загрузка отменена. Загружено %1 из %2."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "Загружено %1 из %2 (пропущено %3)."
msgid "Downloaded %1 of %2."
msgstr "Загружено %1 из %2."
msgid "Removing from cloud…"
msgstr "Удаление из облака…"
@@ -68,6 +68,9 @@ msgstr "ආරෝහණ"
msgid "Actions"
msgstr "ක්‍රියා"
msgid "Download all books"
msgstr "සියලු පොත් බාගන්න"
msgid "Rescan library"
msgstr "පුස්තකාලය නැවත පරිලෝකනය කරන්න"
@@ -134,6 +137,21 @@ msgstr "කවරය බාගත කිරීම අසාර්ථකයි."
msgid "Could not delete the file."
msgstr "ගොනුව මැකිය නොහැක."
msgid "No books to download."
msgstr "බාගත කිරීමට පොත් නැත."
msgid "Downloading %1 of %2…"
msgstr "%2 න් %1 බාගත කරමින්…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "බාගත කිරීම අවලංගු කළා. %2 න් %1 බාගත විය."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%2 න් %1 බාගත විය (%3 මඟ හැරියා)."
msgid "Downloaded %1 of %2."
msgstr "%2 න් %1 බාගත විය."
msgid "Removing from cloud…"
msgstr "වළාකුළුවෙන් ඉවත් කරමින්…"
@@ -68,6 +68,9 @@ msgstr "Naraščajoče"
msgid "Actions"
msgstr "Dejanja"
msgid "Download all books"
msgstr "Prenesi vse knjige"
msgid "Rescan library"
msgstr "Znova preglej knjižnico"
@@ -134,6 +137,21 @@ msgstr "Prenos naslovnice ni uspel."
msgid "Could not delete the file."
msgstr "Datoteke ni bilo mogoče izbrisati."
msgid "No books to download."
msgstr "Ni knjig za prenos."
msgid "Downloading %1 of %2…"
msgstr "Prenašanje %1 od %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Prenos preklican. Prenesenih %1 od %2."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "Prenesenih %1 od %2 (preskočenih %3)."
msgid "Downloaded %1 of %2."
msgstr "Prenesenih %1 od %2."
msgid "Removing from cloud…"
msgstr "Odstranjevanje iz oblaka…"
@@ -68,6 +68,9 @@ msgstr "Stigande"
msgid "Actions"
msgstr "Åtgärder"
msgid "Download all books"
msgstr "Ladda ned alla böcker"
msgid "Rescan library"
msgstr "Skanna biblioteket igen"
@@ -134,6 +137,21 @@ msgstr "Nedladdning av omslag misslyckades."
msgid "Could not delete the file."
msgstr "Kunde inte ta bort filen."
msgid "No books to download."
msgstr "Inga böcker att ladda ned."
msgid "Downloading %1 of %2…"
msgstr "Laddar ned %1 av %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Nedladdning avbruten. %1 av %2 nedladdade."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "Laddade ned %1 av %2 (hoppade över %3)."
msgid "Downloaded %1 of %2."
msgstr "Laddade ned %1 av %2."
msgid "Removing from cloud…"
msgstr "Tar bort från molnet …"
@@ -68,6 +68,9 @@ msgstr "ஏறுநிலை"
msgid "Actions"
msgstr "செயல்கள்"
msgid "Download all books"
msgstr "அனைத்து புத்தகங்களையும் பதிவிறக்கு"
msgid "Rescan library"
msgstr "நூலகத்தை மீண்டும் ஸ்கேன் செய்"
@@ -134,6 +137,21 @@ msgstr "அட்டை பதிவிறக்கம் தோல்விய
msgid "Could not delete the file."
msgstr "கோப்பை நீக்க முடியவில்லை."
msgid "No books to download."
msgstr "பதிவிறக்க புத்தகங்கள் எதுவும் இல்லை."
msgid "Downloading %1 of %2…"
msgstr "%2-இல் %1 பதிவிறக்கப்படுகிறது…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "பதிவிறக்கம் ரத்து செய்யப்பட்டது. %2-இல் %1 பதிவிறக்கப்பட்டது."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%2-இல் %1 பதிவிறக்கப்பட்டது (%3 தவிர்க்கப்பட்டது)."
msgid "Downloaded %1 of %2."
msgstr "%2-இல் %1 பதிவிறக்கப்பட்டது."
msgid "Removing from cloud…"
msgstr "கிளவுட்டிலிருந்து நீக்கப்படுகிறது…"
@@ -68,6 +68,9 @@ msgstr "จากน้อยไปมาก"
msgid "Actions"
msgstr "การกระทำ"
msgid "Download all books"
msgstr "ดาวน์โหลดหนังสือทั้งหมด"
msgid "Rescan library"
msgstr "สแกนคลังใหม่"
@@ -134,6 +137,21 @@ msgstr "ดาวน์โหลดปกไม่สำเร็จ"
msgid "Could not delete the file."
msgstr "ลบไฟล์ไม่ได้"
msgid "No books to download."
msgstr "ไม่มีหนังสือให้ดาวน์โหลด"
msgid "Downloading %1 of %2…"
msgstr "กำลังดาวน์โหลด %1 จาก %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "ยกเลิกการดาวน์โหลดแล้ว ดาวน์โหลด %1 จาก %2 แล้ว"
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "ดาวน์โหลด %1 จาก %2 แล้ว (ข้าม %3)"
msgid "Downloaded %1 of %2."
msgstr "ดาวน์โหลด %1 จาก %2 แล้ว"
msgid "Removing from cloud…"
msgstr "กำลังลบจากคลาวด์…"
@@ -68,6 +68,9 @@ msgstr "Artan"
msgid "Actions"
msgstr "İşlemler"
msgid "Download all books"
msgstr "Tüm kitapları indir"
msgid "Rescan library"
msgstr "Kütüphaneyi yeniden tara"
@@ -134,6 +137,21 @@ msgstr "Kapak indirilemedi."
msgid "Could not delete the file."
msgstr "Dosya silinemedi."
msgid "No books to download."
msgstr "İndirilecek kitap yok."
msgid "Downloading %1 of %2…"
msgstr "%2 kitaptan %1. indiriliyor…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "İndirme iptal edildi. %2 kitaptan %1 tanesi indirildi."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%2 kitaptan %1 tanesi indirildi (%3 tanesi atlandı)."
msgid "Downloaded %1 of %2."
msgstr "%2 kitaptan %1 tanesi indirildi."
msgid "Removing from cloud…"
msgstr "Buluttan kaldırılıyor…"
@@ -68,6 +68,9 @@ msgstr "За зростанням"
msgid "Actions"
msgstr "Дії"
msgid "Download all books"
msgstr "Завантажити всі книги"
msgid "Rescan library"
msgstr "Пересканувати бібліотеку"
@@ -134,6 +137,21 @@ msgstr "Не вдалося завантажити обкладинку."
msgid "Could not delete the file."
msgstr "Не вдалося видалити файл."
msgid "No books to download."
msgstr "Немає книг для завантаження."
msgid "Downloading %1 of %2…"
msgstr "Завантаження %1 з %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Завантаження скасовано. Завантажено %1 з %2."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "Завантажено %1 з %2 (пропущено %3)."
msgid "Downloaded %1 of %2."
msgstr "Завантажено %1 з %2."
msgid "Removing from cloud…"
msgstr "Видалення з хмари…"
@@ -68,6 +68,9 @@ msgstr "Oʻsish boʻyicha"
msgid "Actions"
msgstr "Amallar"
msgid "Download all books"
msgstr "Barcha kitoblarni yuklab olish"
msgid "Rescan library"
msgstr "Kutubxonani qayta skanerlash"
@@ -134,6 +137,21 @@ msgstr "Muqovani yuklab olib boʻlmadi."
msgid "Could not delete the file."
msgstr "Faylni oʻchirib boʻlmadi."
msgid "No books to download."
msgstr "Yuklab olish uchun kitob yoʻq."
msgid "Downloading %1 of %2…"
msgstr "%2 tadan %1-si yuklab olinmoqda…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Yuklab olish bekor qilindi. %2 tadan %1 tasi yuklab olindi."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "%2 tadan %1 tasi yuklab olindi (%3 tasi oʻtkazib yuborildi)."
msgid "Downloaded %1 of %2."
msgstr "%2 tadan %1 tasi yuklab olindi."
msgid "Removing from cloud…"
msgstr "Bulutdan olib tashlanmoqda…"
@@ -68,6 +68,9 @@ msgstr "Tăng dần"
msgid "Actions"
msgstr "Tác vụ"
msgid "Download all books"
msgstr "Tải xuống tất cả sách"
msgid "Rescan library"
msgstr "Quét lại thư viện"
@@ -134,6 +137,21 @@ msgstr "Tải bìa thất bại."
msgid "Could not delete the file."
msgstr "Không thể xóa tệp."
msgid "No books to download."
msgstr "Không có sách nào để tải xuống."
msgid "Downloading %1 of %2…"
msgstr "Đang tải xuống %1 trên %2…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "Đã hủy tải xuống. Đã tải %1 trên %2."
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "Đã tải %1 trên %2 (bỏ qua %3)."
msgid "Downloaded %1 of %2."
msgstr "Đã tải %1 trên %2."
msgid "Removing from cloud…"
msgstr "Đang xóa khỏi đám mây…"
@@ -68,6 +68,9 @@ msgstr "升序"
msgid "Actions"
msgstr "操作"
msgid "Download all books"
msgstr "下载所有书籍"
msgid "Rescan library"
msgstr "重新扫描书库"
@@ -134,6 +137,21 @@ msgstr "封面下载失败。"
msgid "Could not delete the file."
msgstr "无法删除文件。"
msgid "No books to download."
msgstr "没有可下载的书籍。"
msgid "Downloading %1 of %2…"
msgstr "正在下载第 %1 / %2 本…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "下载已取消。已下载 %2 本中的 %1 本。"
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "已下载 %2 本中的 %1 本(跳过 %3 本)。"
msgid "Downloaded %1 of %2."
msgstr "已下载 %2 本中的 %1 本。"
msgid "Removing from cloud…"
msgstr "正在从云端删除…"
@@ -68,6 +68,9 @@ msgstr "遞增"
msgid "Actions"
msgstr "動作"
msgid "Download all books"
msgstr "下載所有書籍"
msgid "Rescan library"
msgstr "重新掃描書庫"
@@ -134,6 +137,21 @@ msgstr "封面下載失敗。"
msgid "Could not delete the file."
msgstr "無法刪除檔案。"
msgid "No books to download."
msgstr "沒有可下載的書籍。"
msgid "Downloading %1 of %2…"
msgstr "正在下載第 %1 / %2 本…"
msgid "Download cancelled. %1 of %2 downloaded."
msgstr "下載已取消。已下載 %2 本中的 %1 本。"
msgid "Downloaded %1 of %2 (skipped %3)."
msgstr "已下載 %2 本中的 %1 本(略過 %3 本)。"
msgid "Downloaded %1 of %2."
msgstr "已下載 %2 本中的 %1 本。"
msgid "Removing from cloud…"
msgstr "正在從雲端移除…"
@@ -539,6 +539,73 @@ describe("LibraryStore", function()
end)
end)
-- =====================================================================
-- listCloudOnlyBooks — the bulk-download candidate set (#4751)
-- =====================================================================
-- A cloud-only book is downloadable but not yet on this device:
-- cloud_present=1, local_present=0, deleted_at IS NULL, and uploaded_at
-- set (the file actually exists in storage — a phantom record has none).
describe("listCloudOnlyBooks", function()
local store
before_each(function()
store = LibraryStore.new({ user_id = "alice" })
-- cloud-only, downloadable → included
store:upsertBook(book({
hash = "h1", title = "Foundation", author = "Asimov",
cloud_present = 1, local_present = 0, updated_at = T_OLD,
}))
-- on device already (cloud + local) → excluded
store:upsertBook(book({
hash = "h2", title = "Dune", author = "Herbert",
cloud_present = 1, local_present = 1, updated_at = T_RECENT,
}))
-- local-only (never uploaded) → excluded
store:upsertBook(book({
hash = "h3", title = "Hyperion", author = "Simmons",
local_present = 1, updated_at = T_MID,
}))
end)
after_each(function() store:close() end)
it("returns only cloud-present, not-local books", function()
local rows = store:listCloudOnlyBooks()
assert.are.equal(1, #rows)
assert.are.equal("Foundation", rows[1].title)
end)
it("excludes a phantom cloud record with no uploaded file", function()
store:upsertBook(book({
hash = "h-phantom", title = "Phantom",
cloud_present = 1, uploaded_at = false, local_present = 0,
}))
local rows = store:listCloudOnlyBooks()
assert.are.equal(1, #rows)
assert.are.equal("Foundation", rows[1].title)
end)
it("excludes cloud-deleted rows even when not yet local", function()
store:upsertBook(book({
hash = "h1", title = "Foundation",
cloud_present = 0, local_present = 0,
deleted_at = T_RECENT, _force_cloud_present = true,
}))
local rows = store:listCloudOnlyBooks()
assert.are.equal(0, #rows)
end)
it("scopes to the current user", function()
local bob = LibraryStore.new({ user_id = "bob" })
assert.are.equal(0, #bob:listCloudOnlyBooks())
bob:close()
end)
it("returns rows carrying the fields downloadBook needs", function()
local rows = store:listCloudOnlyBooks()
assert.are.equal("h1", rows[1].hash)
assert.are.equal("EPUB", rows[1].format)
end)
end)
-- =====================================================================
-- getGroups
-- =====================================================================