From f826b0b655890ad01beac7ab06b8ce50d3d9beb1 Mon Sep 17 00:00:00 2001 From: wasu-code <61418403+wasu-code@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:09:48 +0100 Subject: [PATCH 1/4] feat: add new source: Wolne Lektury --- src/pl/WolneLektury.lua | 200 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 src/pl/WolneLektury.lua diff --git a/src/pl/WolneLektury.lua b/src/pl/WolneLektury.lua new file mode 100644 index 0000000..77a9069 --- /dev/null +++ b/src/pl/WolneLektury.lua @@ -0,0 +1,200 @@ +-- {"id":23119218,"ver":"0.0.0","libVer":"1.0.0","author":"wasu-code","repo":"","dep":["dkjson>=1.0.1"]} + +local qs = Require("url").querystring +local json = Require("dkjson") +local HTMLToString = Require("unhtml").HTMLToString +local FilterOptions = Require("FilterOptions") + +local PAGE_SIZE = 20 + +local baseURL = "https://wolnelektury.pl" + +local FID_SORT = 2 + -- alpha OR -alpha OR popularity OR -popularity +local sortFilter = FilterOptions({ + nil, + { alpha = "Alfabetyczne" }, + { ["-alpha"] = "Alfabetyczne (odwrotne)" }, + { popularity = "Najpopularniejsze" }, + { ["-popularity"] = "Najmniej popularne" }, +}, "Domyślne") + +local function shrinkURL(url, type) + return url + :gsub(".-wolnelektury%.pl/?", "") +end + +local function expandURL(url, type) + return baseURL .. "/" .. url:gsub("^/", "") +end + +--- Generic function to create class tables +--- Creates a Lua "class" table with methods and callable constructor +--- @param methods table? Table containing initial methods (optional) +--- @return table A class-like table with `__index` and `__call` +local function makeClass(methods) + local cls = methods or {} + cls.__index = cls + + -- Make cls callable: cls(table) sets metatable + setmetatable(cls, { + __call = function(self, entry) + setmetatable(entry, self) + return entry + end + }) + + return cls +end + +---@class Book +---@field slug string +---@field title string novel or chapter title +---@field full_sort_key string +---@field href string absolute API URL +---@field url string absolute URL to entry page (HTML) +---@field language string +---@field authors Author[] +---@field translators table +---@field epochs table +---@field genres table +---@field kinds table +---@field children table +---@field parent string|nil +---@field preview boolean +---@field epub string absolute URL to file +---@field mobi string absolute URL to file +---@field pdf string absolute URL to file +---@field html string absolute URL to file +---@field txt string absolute URL to file +---@field fb2 string absolute URL to file +---@field xml string absolute URL to file +---@field cover_thumb string +---@field cover string +---@field isbn_pdf string|nil +---@field isbn_epub string|nil +---@field isbn_mobi string|nil +---@field abstract string|HTML +---@field has_mp3_file boolean +---@field has_sync_file boolean +---@field elevenreader_link string absolute URL to external reader +---@field content_warnings table +---@field audiences table +---@field changed_at string +---@field read_time number +---@field pages number +---@field redakcja string absolute URL (may lead to 404) +local Book = makeClass() + +---@class Chapter +---@field slug string +---@field title string +local Chapter = makeClass() + +function Chapter:toNovelChapter() + return NovelChapter { + title = self.title, + link = self.slug, + } +end + +---Creates Novel object by using a subset of basic fields from Book +---@return Novel novel +function Book:toNovel() + return Novel { + title = self.title, + link = shrinkURL(self.parent or self.href), + imageURL = self.cover_thumb + } +end + +---Creates NovelInfo object from Book fields +---@return NovelInfo novel +function Book:toNovelInfo() + return NovelInfo { + title = self.title, + link = shrinkURL(self.parent or self.href), + imageURL = self.cover, + authors = map(self.authors, function(a) return a.name end), + chapterCount = #self.children, + chapters = ( #self.children > 0 ) + and map(self.children, function(v) + return Chapter(v):toNovelChapter() + end) + or { NovelChapter {title = self.title, link = self.slug} }, + description = HTMLToString(self.abstract), + genres = map(self.genres, function(g) return g.name end), + tags = map(self.kinds, function(k) return k.name end), + language = self.language, + status = NovelStatus.COMPLETED + } +end + +---@class Author +---@field id number +---@field url string +---@field href string +---@field name string +---@field slug string +local Author = {} + +local function getListing(data) + local url = qs({ + offset = PAGE_SIZE * data[PAGE], + sort = sortFilter:valueOf(data[FID_SORT]), + search = data[QUERY], + format = "json", + -- tag = 0 + -- translator = 0 + }, "/api/2/books") + + local jsonData = json.GET(expandURL(url)) + local books = jsonData.member + + if not books then return {} end + + return map(books, function(b) return Book(b):toNovel() end) +end + +local function parseNovel(url, loadChapters) + local jsonData = json.GET(expandURL(url)) + return Book(jsonData):toNovelInfo() +end + +local function getPassage(url) + local documentURL = expandURL("/media/book/html/"..url..".html") + local doc = GETDocument(documentURL) + return pageOfElem(doc, false, ".theme-begin {float:right;}") +end + +return { + id = 23119218, + name = "Wolne Lektury", + baseURL = baseURL, + imageURL = "https://fundacja.wolnelektury.pl/wp-content/themes/koed_wl/images/wolnelektury-favicon.png", + chapterType = ChapterType.HTML, + -- hasCloudFlare = hasCloudFlare, + + shrinkURL = shrinkURL, + expandURL = expandURL, + + listings = { + Listing("Default", true, getListing) + }, + searchFilters = { + DropdownFilter(FID_SORT, "Sortowanie", sortFilter:labels()) + }, + + parseNovel = parseNovel, + getPassage = getPassage, + + hasSearch = true, + isSearchIncrementing = true, + startIndex = 0, + search = getListing, + + settings = {}, + updateSetting = function(id, value) + -- settings[id] = value + end, +} \ No newline at end of file From 06985e64440f1ad280dc8641f023de49ad4a2737 Mon Sep 17 00:00:00 2001 From: wasu-code <61418403+wasu-code@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:46:56 +0100 Subject: [PATCH 2/4] chore: move makeClass utility to separate file --- lib/class.lua | 22 ++++++++++++++++++++++ src/pl/WolneLektury.lua | 27 ++++----------------------- 2 files changed, 26 insertions(+), 23 deletions(-) create mode 100644 lib/class.lua diff --git a/lib/class.lua b/lib/class.lua new file mode 100644 index 0000000..2636202 --- /dev/null +++ b/lib/class.lua @@ -0,0 +1,22 @@ +-- {"ver":"1.0.0","author":"wasu-code"} + +--- Generic function to create class tables +--- Creates a Lua "class" table with methods and callable constructor +--- @param methods table? Table containing initial methods (optional) +--- @return table A class-like table with `__index` and `__call` +local function makeClass(methods) + local cls = methods or {} + cls.__index = cls + + -- Make cls callable: cls(table) sets metatable + setmetatable(cls, { + __call = function(self, entry) + setmetatable(entry, self) + return entry + end + }) + + return cls +end + +return makeClass \ No newline at end of file diff --git a/src/pl/WolneLektury.lua b/src/pl/WolneLektury.lua index 77a9069..24adffd 100644 --- a/src/pl/WolneLektury.lua +++ b/src/pl/WolneLektury.lua @@ -1,16 +1,16 @@ --- {"id":23119218,"ver":"0.0.0","libVer":"1.0.0","author":"wasu-code","repo":"","dep":["dkjson>=1.0.1"]} +-- {"id":23119218,"ver":"0.1.0","libVer":"1.0.0","author":"wasu-code","repo":"","dep":["dkjson>=1.0.1", "class"]} local qs = Require("url").querystring local json = Require("dkjson") local HTMLToString = Require("unhtml").HTMLToString local FilterOptions = Require("FilterOptions") +local makeClass = Require("class") local PAGE_SIZE = 20 local baseURL = "https://wolnelektury.pl" local FID_SORT = 2 - -- alpha OR -alpha OR popularity OR -popularity local sortFilter = FilterOptions({ nil, { alpha = "Alfabetyczne" }, @@ -28,25 +28,6 @@ local function expandURL(url, type) return baseURL .. "/" .. url:gsub("^/", "") end ---- Generic function to create class tables ---- Creates a Lua "class" table with methods and callable constructor ---- @param methods table? Table containing initial methods (optional) ---- @return table A class-like table with `__index` and `__call` -local function makeClass(methods) - local cls = methods or {} - cls.__index = cls - - -- Make cls callable: cls(table) sets metatable - setmetatable(cls, { - __call = function(self, entry) - setmetatable(entry, self) - return entry - end - }) - - return cls -end - ---@class Book ---@field slug string ---@field title string novel or chapter title @@ -161,8 +142,8 @@ local function parseNovel(url, loadChapters) return Book(jsonData):toNovelInfo() end -local function getPassage(url) - local documentURL = expandURL("/media/book/html/"..url..".html") +local function getPassage(slug) + local documentURL = expandURL("/media/book/html/"..slug..".html") local doc = GETDocument(documentURL) return pageOfElem(doc, false, ".theme-begin {float:right;}") end From 0a48e39b2e9541db003076f7731935330ffafcbd Mon Sep 17 00:00:00 2001 From: wasu-code <61418403+wasu-code@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:04:13 +0100 Subject: [PATCH 3/4] feat: use parent's slug as title --- .vscode/shosetsu-ext.code-snippets | 46 +++++++++++++++--------------- src/pl/WolneLektury.lua | 26 +++++++++++++---- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/.vscode/shosetsu-ext.code-snippets b/.vscode/shosetsu-ext.code-snippets index 045bd5c..de4b55d 100644 --- a/.vscode/shosetsu-ext.code-snippets +++ b/.vscode/shosetsu-ext.code-snippets @@ -1,24 +1,24 @@ { - // Place your extensions workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and - // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope - // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is - // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: - // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. - // Placeholders with the same ids are connected. - // Example: - // "Print to console": { - // "scope": "javascript,typescript", - // "prefix": "log", - // "body": [ - // "console.log('$1');", - // "$2" - // ], - // "description": "Log output to console" - // } - "Shosetsu extension template" : { - "scope": "lua", - "prefix": "!ext", - "body": [ + // Place your extensions workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + "Shosetsu extension template": { + "scope": "lua", + "prefix": "--", + "body": [ "-- {\"id\":2311921_,\"ver\":\"0.0.0\",\"libVer\":\"1.0.0\",\"author\":\"wasu-code\",\"repo\":\"\",\"dep\":[]}", "", "local baseURL = \"\"", @@ -69,7 +69,7 @@ " updateSetting = function(id, value)", " -- settings[id] = value", " end,", - "}" + "}", ], - } -} \ No newline at end of file + }, +} diff --git a/src/pl/WolneLektury.lua b/src/pl/WolneLektury.lua index 24adffd..4295044 100644 --- a/src/pl/WolneLektury.lua +++ b/src/pl/WolneLektury.lua @@ -7,6 +7,7 @@ local FilterOptions = Require("FilterOptions") local makeClass = Require("class") local PAGE_SIZE = 20 +local KEY_LISTING_URL = 3 local baseURL = "https://wolnelektury.pl" @@ -25,9 +26,21 @@ local function shrinkURL(url, type) end local function expandURL(url, type) + -- if no type provided it's probably Novel WebView + -- if not type then + -- baseURL.."/katalog/lektura/"..slug, + -- end + -- KEY_NOVEL_URL: baseURL".."/api/2/books/"..slug.."/?format=json" + -- KEY_CHAPTER_URL: baseURL.."/media/book/html/"..slug..".html + -- KEY_LISTING_URL baseURL.."/api/2/"..slug return baseURL .. "/" .. url:gsub("^/", "") end +--- Extracts slug from API URL +local function extractSlug(url) + return url:match("/books/([^/?]+)") +end + ---@class Book ---@field slug string ---@field title string novel or chapter title @@ -41,7 +54,7 @@ end ---@field genres table ---@field kinds table ---@field children table ----@field parent string|nil +---@field parent string|nil absolute API URL to parent entry/book ---@field preview boolean ---@field epub string absolute URL to file ---@field mobi string absolute URL to file @@ -83,7 +96,8 @@ end ---@return Novel novel function Book:toNovel() return Novel { - title = self.title, + -- for chapters use parent's slug in place of title + title = self.parent and extractSlug(self.parent) or self.title, link = shrinkURL(self.parent or self.href), imageURL = self.cover_thumb } @@ -129,21 +143,23 @@ local function getListing(data) -- translator = 0 }, "/api/2/books") - local jsonData = json.GET(expandURL(url)) + local jsonData = json.GET(expandURL(url, KEY_LISTING_URL)) local books = jsonData.member if not books then return {} end + -- TODO: deduplicate (chapters of the same book) and convert to Novel + return map(books, function(b) return Book(b):toNovel() end) end local function parseNovel(url, loadChapters) - local jsonData = json.GET(expandURL(url)) + local jsonData = json.GET(expandURL(url, KEY_NOVEL_URL)) return Book(jsonData):toNovelInfo() end local function getPassage(slug) - local documentURL = expandURL("/media/book/html/"..slug..".html") + local documentURL = expandURL("/media/book/html/"..slug..".html", KEY_CHAPTER_URL) local doc = GETDocument(documentURL) return pageOfElem(doc, false, ".theme-begin {float:right;}") end From d29335d06655adc0b579a9d5019491c25fed33f1 Mon Sep 17 00:00:00 2001 From: wasu-code <61418403+wasu-code@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:40:28 +0100 Subject: [PATCH 4/4] feat: naive deduplication & better titles for series --- src/pl/WolneLektury.lua | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/pl/WolneLektury.lua b/src/pl/WolneLektury.lua index 4295044..e666f82 100644 --- a/src/pl/WolneLektury.lua +++ b/src/pl/WolneLektury.lua @@ -37,8 +37,13 @@ local function expandURL(url, type) end --- Extracts slug from API URL -local function extractSlug(url) - return url:match("/books/([^/?]+)") +local function extractTitleFromURL(url) + return url + :match("/books/([^/?]+)") -- extract slug + :gsub("-", " ") -- replace hyphens with spaces + :gsub("(%w)(%w*)", function(first, rest) -- capitalize first letters + return first:upper() .. rest:lower() + end) end ---@class Book @@ -97,7 +102,7 @@ end function Book:toNovel() return Novel { -- for chapters use parent's slug in place of title - title = self.parent and extractSlug(self.parent) or self.title, + title = self.parent and extractTitleFromURL(self.parent) or self.title, link = shrinkURL(self.parent or self.href), imageURL = self.cover_thumb } @@ -148,9 +153,18 @@ local function getListing(data) if not books then return {} end - -- TODO: deduplicate (chapters of the same book) and convert to Novel - - return map(books, function(b) return Book(b):toNovel() end) + -- that will get rid of duplicates only on THIS page + -- some duplicates may still occur if spread through multiple pages + local seen = {} + local novels = {} + for _, b in ipairs(books) do + if not b.parent or not seen[b.parent] then + table.insert(novels, Book(b):toNovel()) + end + seen[b.parent or b.href] = true + end + + return novels end local function parseNovel(url, loadChapters)