Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save charleshan/f981b64d42bff8c6d0ad96738a6c945b to your computer and use it in GitHub Desktop.

Select an option

Save charleshan/f981b64d42bff8c6d0ad96738a6c945b to your computer and use it in GitHub Desktop.
Fix for KOReader Wallabag deleting archive even when not told to + downloading Wallabag Archive
--[[
Wallabag: respect "Delete remotely archived and deleted articles locally"
(sync_remote_archive) when auto-uploading statuses.
Bug: uploadStatuses() always deleted/archived local files after uploading a
status to the server, even when sync_remote_archive was disabled — ignoring
the setting's label. This patch makes the local deletion conditional on
sync_remote_archive being on.
How the patching works
----------------------
The Wallabag plugin is loaded via dofile(), not require(). We use KOReader's
supported hook userpatch.registerPatchPluginFunc(plugin_name, fn), which is
called with the plugin's class table after an instance is created, so we can
wrap the method in place without touching any global state.
Targeted at the wallabag.koplugin shipped in KOReader 2026.03.x, whose API has
uploadStatuses(), archiveLocalArticle()/deleteLocalArticle() and the
sync_remote_archive setting.
Install: copy to koreader/patches/1-wallabag-dont-delete-local-on-upload.lua
]]
local userpatch = require("userpatch")
userpatch.registerPatchPluginFunc("wallabag", function(Wallabag)
-- registerPatchPluginFunc runs this on every createPluginInstance() call,
-- so guard against wrapping the (shared) class method more than once.
if Wallabag._sync_remote_archive_patched then return end
Wallabag._sync_remote_archive_patched = true
-- We patch uploadStatuses() directly rather than the two helper functions
-- (archiveLocalArticle / deleteLocalArticle) because those helpers are also
-- called by processRemoteDeletes(), which is already correctly gated by
-- sync_remote_archive at its call site in downloadArticles().
local orig_uploadStatuses = Wallabag.uploadStatuses
Wallabag.uploadStatuses = function(self, ...)
if self.sync_remote_archive then
-- sync_remote_archive is on: original behaviour, delete locally
return orig_uploadStatuses(self, ...)
end
-- sync_remote_archive is off: upload status to server but keep local
-- file. Temporarily replace the two deletion helpers with no-ops for
-- the duration of this call only.
local orig_archiveLocal = self.archiveLocalArticle
local orig_deleteLocal = self.deleteLocalArticle
self.archiveLocalArticle = function() return 0 end
self.deleteLocalArticle = function() return 0 end
local count_remote, count_local = orig_uploadStatuses(self, ...)
self.archiveLocalArticle = orig_archiveLocal
self.deleteLocalArticle = orig_deleteLocal
return count_remote, count_local
end
end)
--[[
Wallabag: "Also download archived articles" toggle.
Adds Settings > Download settings > Also download archived articles.
When on, fetches both unread AND archived articles. Default off.
How the patching works
----------------------
The Wallabag plugin is loaded via dofile(), not require(), so the standard
userpatch require("main") trick can't reach the live class. KOReader provides
the supported hook userpatch.registerPatchPluginFunc(plugin_name, fn): fn is
called with the plugin's class table right after a plugin instance is created,
so we can patch methods in place without touching any global state.
Targeted at the wallabag.koplugin shipped in KOReader v2026.03-43
(g6101f9acc). That version persists settings via saveSettings() (NOT
onFlushSettings / self.updated): each Settings toggle flips a field then calls
self:saveSettings(), which rebuilds its save table inline. We mirror that.
Install: copy to koreader/patches/1-wallabag-download-archived.lua
]]
local userpatch = require("userpatch")
userpatch.registerPatchPluginFunc("wallabag", function(Wallabag)
-- registerPatchPluginFunc runs this on every createPluginInstance() call,
-- so guard against patching the (shared) class table more than once —
-- otherwise method wrappers nest and the menu item is inserted repeatedly.
if Wallabag._download_archived_patched then return end
Wallabag._download_archived_patched = true
-- Read the persisted setting from saved settings, defaulting to false.
-- Done lazily (rather than by wrapping init) because the very first
-- instance each launch is created *before* this patch func runs, so its
-- init has already finished; lazy-loading covers that instance too.
local function downloadArchived(self)
if self.download_archived == nil then
local saved = self.wb_settings
and self.wb_settings.data
and self.wb_settings.data.wallabag
and self.wb_settings.data.wallabag.download_archived
self.download_archived = saved or false
end
return self.download_archived
end
-- 1. Persist the setting -------------------------------------------------
-- This version has no onFlushSettings; toggles persist by calling
-- self:saveSettings(), which rebuilds its save table inline from self.*
-- and so omits our key. Let the original save+flush run, then inject our
-- key into the just-written in-memory data and flush once more.
local orig_save = Wallabag.saveSettings
Wallabag.saveSettings = function(self, ...)
orig_save(self, ...)
if self.wb_settings and self.wb_settings.data
and self.wb_settings.data.wallabag then
self.wb_settings.data.wallabag.download_archived = downloadArchived(self)
self.wb_settings:flush()
end
end
-- 2. Remove archive=0 filter when the setting is on ---------------------
-- The article list URL is built as "/api/entries.json?archive=0&page=…"
-- We intercept callAPI and strip the filter for that specific endpoint.
local orig_callAPI = Wallabag.callAPI
Wallabag.callAPI = function(self, method, url, ...)
if downloadArchived(self)
and method == "GET"
and type(url) == "string"
and url:match("/api/entries%.json%?archive=0&") then
-- strip "?archive=0&" → "?" leaving the remaining params intact
url = url:gsub("/api/entries%.json%?archive=0&",
"/api/entries.json?")
end
return orig_callAPI(self, method, url, ...)
end
-- 3. Inject menu toggle into Download settings --------------------------
local orig_menu = Wallabag.addToMainMenu
Wallabag.addToMainMenu = function(self, menu_items, ...)
orig_menu(self, menu_items, ...)
local _ = require("gettext")
local wallabag_menu = menu_items.wallabag
if not (wallabag_menu and wallabag_menu.sub_item_table) then return end
-- NB: use __ / ___ (not _) for the loop indices — _ is gettext above,
-- and a "for _, item" loop variable would shadow it, turning
-- _("Settings") into a call on a number (crash) when the menu is built.
for __, item in ipairs(wallabag_menu.sub_item_table) do
if item.text == _("Settings") and item.sub_item_table then
for ___, sub in ipairs(item.sub_item_table) do
if sub.text == _("Download settings") and sub.sub_item_table then
table.insert(sub.sub_item_table, {
text = _("Also download archived articles"),
help_text = _(
"Download both unread and archived articles from the server.\n"
.. "When disabled (default), only unread articles are downloaded."
),
keep_menu_open = true,
checked_func = function()
return downloadArchived(self)
end,
callback = function()
self.download_archived = not downloadArchived(self)
self:saveSettings()
end,
})
return -- done, stop searching
end
end
end
end
end
end)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment