Skip to content

Filesearcher: add search in book metadata#10198

Merged
hius07 merged 26 commits into
koreader:masterfrom
hius07:filesearcher-metadata
Mar 28, 2023
Merged

Filesearcher: add search in book metadata#10198
hius07 merged 26 commits into
koreader:masterfrom
hius07:filesearcher-metadata

Conversation

@hius07

@hius07 hius07 commented Mar 12, 2023

Copy link
Copy Markdown
Member

Unfortunately it is really slow.

01


This change is Reviewable

@NiLuJe

NiLuJe commented Mar 12, 2023

Copy link
Copy Markdown
Member

If the overhead isn't simply in the "get metadata" step, but in the "parsing metadata" one, you might want to restrict the search to very specific fields (e.g., title/author/series). I can certainly see the appeal of including description in there, but that might very well be your bottleneck right there... ;o).

Sidebar: This is nearly instantaneous if done via the calibre plugin/metadata ;p.

@hius07

hius07 commented Mar 12, 2023

Copy link
Copy Markdown
Member Author

Description in fb2 books from Flibusta contains important information like translators, publushers etc.
Maybe make separate checkboxes to search in title, authors, series, description?

@NiLuJe

NiLuJe commented Mar 12, 2023

Copy link
Copy Markdown
Member

Maybe make separate checkboxes to search in title, authors, series, description?

If it actually helps the performance and doesn't look too messy, yeah?

@poire-z

poire-z commented Mar 12, 2023

Copy link
Copy Markdown
Contributor

I don't think that 1 or 4 metadata fields lookup would change anything (and neither html2plaintext) - the bottleneck is opening each book.
So, I'd rather keep that simple and a single checkbox (given the non neglieable amount of EPUBs I have were the title and author are swapped in their metadata).

@Frenzie

Frenzie commented Mar 12, 2023

Copy link
Copy Markdown
Member

Isn't that info in the SQLite DB?

@poire-z

poire-z commented Mar 12, 2023

Copy link
Copy Markdown
Contributor

If the coverbrowser plugin is enabled, it may be, for directories browsed - and it may not for sub/directories not visited or not "Extract and cache book information", or recently added books.
It's also in the docsettings of already opened books.

About the coverbrowser sqlite db, I'd like to mention that if we were to need a proper sqlite database for the purpose of metadata search, or a "library view" (ala Nickel, that some users would prefer to our file browser), I would prefer it to be another sqlite database. It could be a new plugin that could duplicate and extend bookinfomanager.lua if needs be.
Because the coverbrowser db is easy to handle: it's just a cache, we can trash it or entries, forget about deleted or moved books, replace lines when needing thumbnails of a different size - I wouldn't want it to get more complicated because of other reasons.
Another plugin with real care for complete metadata would need to keep track of deletions/move/addition and do regular scans for new books. Moreover, I think it would need to propose metadata edition to users, so they can fix/rewrite crap ones. Then, that metadata becomes more precious, and a lot more care (and more complex SQL) is needed to manage it.

(Or such a DB should be a "core" service, that coverbrowser could be a client to - but then, KOReader would no longer be the KOReader I like :)

@Frenzie

Frenzie commented Mar 12, 2023

Copy link
Copy Markdown
Member

For me the metadata is fine as is. :)

@hius07 hius07 marked this pull request as draft March 15, 2023 19:53
@hius07

hius07 commented Mar 18, 2023

Copy link
Copy Markdown
Member Author

Search results view is now consistent with the file browser view (sorting, filtering, classic display mode).
To avoid code duplication FileChooser has been refactored and some of its methods are used in FileSearch.

FileChooser change: in classic mode files mandatory now shows the parameter by which sorting is done.

Minor wordings changes of the file searcher dialog window:

02

@hius07 hius07 marked this pull request as ready for review March 18, 2023 10:27
@hius07

hius07 commented Mar 18, 2023

Copy link
Copy Markdown
Member Author

On tap in search results:

03

@poire-z

poire-z commented Mar 18, 2023

Copy link
Copy Markdown
Contributor

Fine with the switch of tap/long-press in the results.
For the code changes, too much stuff I'm not familiar with, so trusting you :)

@hius07

hius07 commented Mar 18, 2023

Copy link
Copy Markdown
Member Author

Not a benchmark but rough estimation (a folder with 5 epubs of about 2 MB each): searching in metadata of an unopened book is ~50 times slower comparing to an opened one (with sdr).
Opening a book (even to get props only) is definitely the bottleneck.

@poire-z

poire-z commented Mar 18, 2023

Copy link
Copy Markdown
Contributor

Various thoughts:

You could add a small wrapper around BookInfoManager:getBookInfo(filepath) in coverbrowser's main.lua, so you could use self.ui.coverbrowser:getBookInfo(filepath) if self.ui.coverbrowser is available. You may get stalled info if the book metadata has been edited later, but so you would when reading from docsettings if the book has not been reopened since.
You could count the number of "miss" where you needed to open the book, and show a warning "You can speed things up by using Extract and cache book information".
(This would do a sql query for each book, so 1000 select if 1000 books - instead of using a single sql with title like '%metadata%' and authors like %metadata%', but that's bearable and still a lot speeder than opening each book.)

You could also force this extraction for each book - although that might be complicated and ugly (it's done in a background subprocess asynchronously, and it usually fetches the cover with the expected size for the current coverbrowser view).

Or you could even not rely on coverbrowser, and just cache (dump()/load()) a big table with all the metadata you have extracted once - so it is "slow" only the first time it is used, and when new files are met.

Also, I haven't look at all the code, but I guess this is blocking: if you have 300 never opened book, KOReader will go at opening the 300 of them, and the user can't stop that - and he may get stuck for 15mn...
Solutions for that are either using Trapper, or calling the search function with runInSubprocess(), so it can be interrupted by the main process. Both solutions are complicated though :/

@poire-z

poire-z commented Mar 18, 2023

Copy link
Copy Markdown
Contributor

Or you could even not rely on coverbrowser, and just cache (dump()/load()) a big table with all the metadata you have extracted once - so it is "slow" only the first time it is used, and when new files are met.

And if you would do that, it could be a core module, just caching filepath/filetimestamp/{metadata}, that you could just complete and iterate. May be you could even use LuaData to add new item to it, like we do for dict lookup queries.
We would not even need sqlite stuff: it's not that we could have 1 millions of books - loading 5000 books with each 1000 bytes of metadata should be fast.
(For coverbrowser, I needed sqlite mostly to allow communication between the main process (reading the DB to see if the book is done being indexed) and the subprocess (inserting into the DB), and sqlite itself avoiding concurrency with locks and ensuring all works well.)

Frenzie added a commit to Frenzie/koreader that referenced this pull request Mar 18, 2023
…nection

Due to <koreader#10198 (comment)> I browsed the SQLite documentation a bit.

Looking at <https://www.sqlite.org/pragma.html#pragma_optimize>, it says:

> To achieve the best long-term query performance without the need to do a detailed engineering analysis of the application schema and SQL, it is recommended that applications run "PRAGMA optimize" (with no arguments) just before closing each database connection. Long-running applications might also benefit from setting a timer to run "PRAGMA optimize" every few hours.
>
> This pragma is usually a no-op or nearly so and is very fast. However if SQLite feels that performing database optimizations (such as running ANALYZE or creating new indexes) will improve the performance of future queries, then some database I/O may be done. Applications that want to limit the amount of work performed can set a timer that will invoke sqlite3_interrupt() if the pragma goes on for too long. Or, since SQLite 3.32.0, the application can use PRAGMA analysis_limit=N for some small value of N (a few hundred or a few thousand) to limit the depth of analyze.
@hius07

hius07 commented Mar 19, 2023

Copy link
Copy Markdown
Member Author

Thank you.
I believe the cache is needed, this is not good to have such a slow search on every search.
Should we implement the metadata search without the cache and then discuss and enhance the cache realization in a new PR?

(At a first glance I tend to extending the DocSettings module to maintain the new DocPropsCache that would be a new file with hashes [filepath] = {book_props}. It would be updated within DocSettings:update(), and on every search, and by coverbrowser. And manually on a "Scan library" menu item. Needs more thinking and discussing).

@poire-z

poire-z commented Mar 19, 2023

Copy link
Copy Markdown
Contributor

Should we implement the metadata search without the cache and then discuss and enhance the cache realization in a new PR?

Dunno, the current state could block the user for 15 minutes with no way to get out but by powering off, so it feels like a non-feature :/ (unless he looks in a small subtree, or is used to opening all his books).

extending the DocSettings module to maintain the new DocPropsCache that would be a new file with hashes [filepath] = {book_props}.

Feels like the simpler solution, a "cache in the middle" :)
But it also feels limited and redundant with what CoverBrowser has, different code (sqlite vs Lua table) achieving the same purpose (caching metadata) with the same caveats (no editing, no handling of removed/moved books...)

It would be updated within DocSettings:update()

Well, I wouldn't want that big cache file to be rewritten each time I switch books, so you'd have to look if metadata has changed to see if it really need updates.

Needs more thinking and discussing

Yep, and I was happy not having to think about it until you came up with this PR :/ :)
I think if you are going to do that, you should think broader, and keep in mind that it should be reusable by a "Library view" plugin (#8472) and provide anything this could need.
It feels like it then better be a sqlite db. A clone of bookinfomanager.lua (ie. copy that, let CoverBrowser alone in the whole meantime, keeping in mind it should also be usable by coverbrowser, and coverbrowser should switch to using it in the end).
Some thoughs I had yesterday around that idea:

  • the data for a book should have the timestamp of the file, so we can notice the book file mod time has been updated and the cached metadata can be stalled
  • it should have a edited=true/false, and allow the user to edit title/authors, with new text either replacing the one extracted for the book, or may be better: stored in additiona fields
  • the cover bb should may be stored in another SQL table, with versions for the multiple size (ie. small detailed list mode vs mosaic mode - currently, I replace small ones by larger ones, so some downsizing may happen when you browse detailed list)

Anyway, lots of thinking to give it - and not really happy about having to give it that much :)

@Frenzie

Frenzie commented Mar 19, 2023

Copy link
Copy Markdown
Member

I agree with the above. I'm not motivated to create something like a library view, but if you're implementing something along these lines I don't think it's a very good idea unless you take CoverBrowser as a point of departure and add some well considered extra columns.

@poire-z

poire-z commented Mar 19, 2023

Copy link
Copy Markdown
Contributor

Should we implement the metadata search without the cache and then discuss and enhance the cache realization in a new PR?

Dunno, the current state could block the user for 15 minutes with no way to get out but by powering off, so it feels like a non-feature :/ (unless he looks in a small subtree, or is used to opening all his books).

But if you want to keep that (and may be this will be enough, and we can postpone that dreadful thinking :), you could:

  • implement the coverbrowser:getBookInfo(filepath) I mentionned, and use only that (no docsetting opening needed unless you really want to, and surely no book opening)
  • disable this metadata checkbox if coverbrowser not enabled
  • if you got any miss, just report it to the user, and suggest "Extract and cache..."

@NiLuJe

NiLuJe commented Mar 24, 2023

Copy link
Copy Markdown
Member

Possibly because of resetState() (losing the hold state) done by inhibitInput/restoreInput when opening a book - and now when show folder.

Yeah, that sounds sensible. You can't call resetState during a "complex" gesture (i.e., anything that isn't a tap or a key), as it'll break it down, resulting in a tap on contact lift.

(I'll look at the logs tonight to confirm, but that's probably it).

EDIT: Yup, definitely that ;).

@hius07

hius07 commented Mar 24, 2023

Copy link
Copy Markdown
Member Author

I've made a simple benchmark to compare the speed of getting book props from sdr vs coverbrowser sql.
For previously opened books DocSettings:open() is about 9 times slower than coverbrowser:getBookInfo().
I think it makes sense to get info from sql in classic mode too.
And add the "Extract book info..." button to the classic mode Plus menu.

@Frenzie

Frenzie commented Mar 24, 2023

Copy link
Copy Markdown
Member

You mean in the specific scenario of search while in classic mode? Yes, I think it would make sense to say that completely classic search for metadata doesn't make much sense.

Btw, where'd your run the benchmark? If that was on the emulator then DocSettings:open() may have performed significantly better than on a real device due to I/O bottlenecks.

@hius07

hius07 commented Mar 24, 2023

Copy link
Copy Markdown
Member Author

I ran the benchmark on my Kindle Voyage.

I mean using the coverbrowser sql base in the file popup menu in FM, History and Favorites:

{
text = _("Book information"),
id = "book_information", -- used by covermenu
callback = function()
UIManager:close(self.file_dialog)
FileManagerBookInfo:show(file)
end,
},

{
text = _("Book description"),
id = "book_description", -- used by covermenu
callback = function()
UIManager:close(self.file_dialog)
FileManagerBookInfo:onShowBookDescription(nil, file)
end,
},

Currently in classic mode it reads book props from sdr.
I propose at first to try sql, then to try sdr, and then to open a file to get props.

@Frenzie

Frenzie commented Mar 24, 2023

Copy link
Copy Markdown
Member

I mean using the coverbrowser sql base in the file popup menu in FM, History and Favorites:

I suppose? In that case the difference seems immaterial; it feels instantaneous either way. Are you talking about when there's no metadata.lua yet? It seems hard to imagine it'd be 9 times slower?

@hius07

hius07 commented Mar 24, 2023

Copy link
Copy Markdown
Member Author

Yes, the difference is not visible per one file.
Only we would know that we do it faster, with less power consumption, and without code duplication (fudging buttons in CoverBrowser).

@Frenzie

Frenzie commented Mar 24, 2023

Copy link
Copy Markdown
Member

What does your benchmark consist of exactly? I find it hard to imagine that simply parsing a tiny bit of Lua is not only heavier than instantiating all of SQLite but an order of magnitude so. But I have heard that the filesystem on Kindle is unbelievably slow…

@hius07

hius07 commented Mar 24, 2023

Copy link
Copy Markdown
Member Author

It was a previously opened book with metadata extracted to sql.

logger.info("doc_settings start", os.clock())
for i = 1, 1000 do
    local doc_settings = DocSettings:open(item.path)
    local descr = doc_settings:readSetting("doc_props").description
end
logger.info("doc_settings end", os.clock())
logger.info("bookinfo start", os.clock())
for i = 1, 1000 do
    local bookinfo = self._manager.ui.coverbrowser:getBookInfo(item.path)
    local descr = bookinfo.description
end
logger.info("bookinfo end", os.clock())

@NiLuJe

NiLuJe commented Mar 24, 2023

Copy link
Copy Markdown
Member

os.clock may be misleading (it only counts CPU time of the Lua process), which may be very far from the actual real world time ;).

The saner way to do this is via time, e.g.,

--local start_time = time.now()
self._drawn_images_count, self._drawn_images_surface_ratio =
self._document:drawCurrentPage(self.buffer, self.render_color, Screen.night_mode and self._nightmode_images, self._smooth_scaling, Screen.sw_dithering)
--local end_time = time.now()
--print(string.format("CreDocument:drawCurrentView: Rendering took %9.3f ms", time.to_ms(end_time - start_time))

@Frenzie

Frenzie commented Mar 24, 2023

Copy link
Copy Markdown
Member

I don't see such a drastic difference, closer to twice as slow. According to the profiler up to about 25% of the difference comes from getHistoryPath, basename and buildCandidates, but without doing anything about that the benchmark comes to the same result if you limit the metadata to basically just metadata, along these lines (see #2789):

-- we can read Lua syntax here!
return {
    ["doc_pages"] = 250,
    ["doc_path"] = "/home/frans/src/kobo/koreader/test/juliet.epub",
    ["doc_props"] = {
        ["authors"] = "William Shakespeare",
        ["description"] = "Romeo and Juliet is a tragic play written early in the career of William Shakespeare about two teenage \"star-cross'd lovers\" whose untimely deaths ultimately unite their feuding households. It was among Shakespeare's most popular plays during his lifetime and, along with Hamlet, is one of his most frequently performed plays. Today, the title characters are regarded as archetypal \"young lovers\". (From Wikipedia)",
        ["keywords"] = "Fiction\
Drama\
Romance",
        ["language"] = "en",
        ["series"] = "",
        ["title"] = "Romeo and Juliet",
    },
}

But I don't know why you'd make things complicated for something that isn't slow and that you only do irregularly.

PS For fun you can throw a closeDbConnection() in there and watch the time increase by an order of magnitude over the plain file method. I suspect that that's more realistic, at least for my usage. Unless you keep it open even when switching between reader and filemanager.

@hius07

hius07 commented Mar 26, 2023

Copy link
Copy Markdown
Member Author

So, the summary of changes in this PR:

(1) Search files
The dialog includes a checkbox to search in book metadata (default unchecked).
Metadata is taken from the coverbrowser sql base, or from the book sdr file. No book opening is done. The books without metadata in sql/sdr are skipped in search and the dialog proposing to extract the metadata is shown.
Technically: the search results list now is based on the FileChooser code, so all the sorting and filtering parameters are applicable to the list.

(2) File sorting
A new separate checkbox for mix sorting of folders and files, available in all sorting modes except "type", "size", "percent finished", where folders are always sorted by name and shown on top of the list.
In classic mode the mandatory of the file list shows the value of the sorting parameter. So we understand the files order. For sorting by name the file size is shown, as usual.

Works good I hope.

Comment on lines +323 to +324
if DocumentRegistry:hasProvider(item.path) then
UIManager:scheduleIn(0.5, function()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a comment - so we remember that and may be one day find a better solution to it.
-- Workaround an issue with this long-press' release being handled as tap by the Reader we launch

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually not a good workaround, if a long-press is longer than 0.5 sec it doesn't help.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not good, but good enough ? :) Let's not block this PR, we can fix that later if we get a better idea.

Another idea (dunno if it is good).

--- a/frontend/ui/widget/menu.lua
+++ b/frontend/ui/widget/menu.lua
@@ -131,7 +131,7 @@ function MenuItem:init()
         },
         HoldSelect = {
             GestureRange:new{
-                ges = "hold",
+                ges = self.handle_hold_on_hold_release and "hold_release" or "hold",
                 range = self.dimen,
             },
         },

and provide and pass handle_hold_on_hold_release when you know it's best to handle it that way.

I think we mostly everywhere handle long-press before the finger lift - but we have a few places (don't really remember where) where we handle it on finger lift (so, hold_release), probably for good reasons. I remember I had sometimes been surprised by that (waiting, and seeing nothing done, until I lift my finger), and it's a bit inconsistent, but well, it's just fine.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect!

@hius07 hius07 merged commit 4d26650 into koreader:master Mar 28, 2023
@hius07 hius07 deleted the filesearcher-metadata branch March 28, 2023 13:17
@hius07 hius07 added this to the 2023.04 milestone Mar 28, 2023
@hius07

hius07 commented Apr 1, 2023

Copy link
Copy Markdown
Member Author

The changes to FileChooser in this PR have broken the unreadable dir content handling (like going up from Android /storage/emulated/0).
The piece of code that handles the "unreadable dir content" was not changed (pcall and ok check).
I cannot reproduce the bug on Kindle, there are no unreadable partitions here.

@poire-z

poire-z commented Apr 1, 2023

Copy link
Copy Markdown
Contributor

I can simulate that on the linux emulator with chmod 111 some upper parent directory.
The crash I get when going up is:

./luajit: frontend/ui/widget/filechooser.lua:271: attempt to concatenate field 'text' (a nil value)
stack traceback:
 frontend/ui/widget/filechooser.lua:271: in function 'genItemTableFromPath'
 frontend/ui/widget/filechooser.lua:391: in function 'refreshPath'
 frontend/ui/widget/filechooser.lua:415: in function 'changeToPath'
 frontend/ui/widget/filechooser.lua:474: in function 'onMenuSelect'

Hope this helps :)

Comment on lines +892 to +895
local start_with = G_reader_settings:readSetting("start_with")
for i, v in ipairs(start_withs) do
if v[2] == start_with then
return T(_("Start with: %1"), v[1])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A default is missing. Cf. #10368.

The old one said:

    local start_with_setting = G_reader_settings:readSetting("start_with") or "filemanager"

Frenzie added a commit to Frenzie/koreader that referenced this pull request Apr 29, 2023
Frenzie added a commit that referenced this pull request Apr 30, 2023
TranHHoang added a commit to TranHHoang/koreader that referenced this pull request May 26, 2023
KOReader 2023.04 "Solar Panel"

![koreader-2023-04-fs8](https://user-images.githubusercontent.com/202757/234975122-d7739c71-74aa-4cb0-a086-72a4bba70f52.png)

It's been another busy month squashing many bugs. Our Mac users will be happy to hear that I told macOS we've supported HiDPI since long before anyone came up with such terminology (koreader#10341), and that the program can now natively build on M1 devices (koreader#10291).

Solar panel credit: <https://openclipart.org/detail/294030/solar-energy> by gnokii

We'd like to thank all contributors for their efforts. Some highlights since the previous release include:

* Readerzooming: fix use of default settings (koreader#10205) @hius07
* ButtonDialog/ButtonDialogTitle: consistent 'width' handling (koreader#10230) @poire-z
* MovableContainer: add support for anchoring initial position (koreader#10230) @poire-z
* Book map, Page browser: add top left menu (koreader#10230) @poire-z
* Book style tweak: add button with CSS suggestions (koreader#10230) @poire-z
* crengine: fix parsing of multibytes encodings (koreader#10230) @poire-z
* readerstyletweak: update profiles on unregistering in dispatcher (koreader#10247) @hius07
* Filesearcher: add search in book metadata (koreader#10198) @hius07
* Fix: Updated legacy directory, which crashed the program (koreader#10260) @Mochitto
* ReaderLink: allow a forward location stack (koreader#10228) @yparitcher
* BookInfo: add page information (koreader#10255) @hius07
* Center pdf manual zoom mode (koreader#10246) @nairyosangha
* File browser: add Folder Menu (koreader#10275) @hius07
* Calendar view: add options to change start time of days (koreader#10254) @weijiuqiao
* filechooser: fix crash on "unreadable content" (koreader#10283) @hius07
* Sync book statistics: add to dispatcher (koreader#10285) @ptrm
* [plugin] Exporter: use util.getSafeFilename() to remove illegal characters from output filename (koreader#10282) @Mochitto
* readerbookmark: fix writing pdf annotation (koreader#10287) @hius07
* Folder Menu: sign for Home folder (koreader#10288) @hius07
* ListMenu: show mark for books with highlights (koreader#10276) @hius07
* Calendar view's day view: thicker separator at 00:00 (koreader#10289) @poire-z
* PageBrowser: tweak scrolling behaviour at book start/end (koreader#10289) @poire-z
* Make `kodev check` feature complete (koreader#8682) @yparitcher
* macOS: support for M1 building (koreader#10291) @ptrm
* Reader: do not apply font size and spacing out of range (koreader#10295, koreader#10307) @hius07
* File browser: show Folder Menu on long-press on Home icon (koreader#10298) @hius07
* SSH.koplugin: fix cant stop SSH server bug when pid file's stale (koreader#10300) @weijiuqiao
* PM: Optimize task queue handling around standby (koreader#10203) @zwim
* statistic.koplugin: fix today's timeline showing next day when within custom offset (koreader#10299) @weijiuqiao
* ReaderThumbnails: update cached page thumbnail on bookmark note change (koreader#10303) @hius07
* SDL: add multitouch support (koreader#10334) @Frenzie
* SDL: add HiDPI support (koreader#10341) @Frenzie
* BookInfo: fix crash on show cover (koreader#10315) @hius07
* Deal with table.pack corner-cases properly (koreader#10350) @NiLuJe
* Android: add Tagus Gea support (<koreader/android-luajit-launcher#412>) @Alfedi

[Full changelog](koreader/koreader@v2023.03...v2023.04) — [closed milestone issues](https://github.com/koreader/koreader/milestone/63?closed=1)

---

Installation instructions: [Android](https://github.com/koreader/koreader/wiki/Installation-on-Android-devices) • [Cervantes](https://github.com/koreader/koreader/wiki/Installation-on-BQ-devices) • [ChromeOS](https://github.com/koreader/koreader/wiki/Installation-on-Chromebook-devices) • [Kindle](https://github.com/koreader/koreader/wiki/Installation-on-Kindle-devices) • [Kobo](https://github.com/koreader/koreader/wiki/Installation-on-Kobo-devices) • [PocketBook](https://github.com/koreader/koreader/wiki/Installation-on-PocketBook-devices) • [ReMarkable](https://github.com/koreader/koreader/wiki/Installation-on-ReMarkable) • [Desktop Linux](https://github.com/koreader/koreader/wiki/Installation-on-desktop-linux) • [MacOS](https://github.com/koreader/koreader/wiki/Installation-on-MacOS)
0xstillb pushed a commit to 0xstillb/koreader-thai that referenced this pull request May 9, 2026
0xstillb pushed a commit to 0xstillb/koreader-thai that referenced this pull request May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants