Compare commits

...
8 Commits
Author SHA1 Message Date
tangent 1c7e66e38e Update ReadMe.md
Make it more clear how the fractional part of hours is intended to be minutes. Whoops.
2026-08-06 18:39:19 -06:00
tangent e1fa8e1916 beginning support for hours - for audiobook tracking 2026-08-06 18:37:30 -06:00
tangent 8dce67b2c4 customizable number of items to print and access on launch
this also fixes an issue where item 10 (displayed as 0) was unselectable
2026-08-06 18:16:42 -06:00
tangent ae484aa134 reorganizing 2026-08-06 17:59:36 -06:00
tangent 7e3b9f43d1 launch() needs to save when it corrects the data structure so it doesn't have to do it every time 2026-08-06 16:43:49 -06:00
tangent 9c2d209de3 How did I forget this end? -.-
The gitea editor was bugging out with character entry,
but I didn't expect it to fail a whole word. It probably was
on me, but idk because of that.
2026-08-06 16:41:23 -06:00
tangent 5fa8c1a974 close #13 making the flow of sort functions more obvious
The commit closing #6 was somehow broken anyhow.
2026-08-06 16:39:31 -06:00
tangent 20f70aed7f close #5 the data knows where it is, and the file doesn't 2026-08-04 21:15:05 -06:00
4 changed files with 124 additions and 85 deletions
+5 -1
View File
@@ -37,19 +37,23 @@ Licensing? I don't care. Do whatever you want. IP rights are stupid.
"series": "Series", "series": "Series",
"genre": "Fiction", "genre": "Fiction",
"pages": 100, "pages": 100,
"hours": 1.30,
"priority": 0, "priority": 0,
"progress": 0.5, "progress": 0.5,
"hidden": true "hidden": true
} }
``` ```
- `title`, `author`, `series`, `pages`, `genre`, and `hidden` are optional. - `title`, `author`, `series`, `pages`, `hours`, `genre`, and `hidden` are
optional.
- At least one of `title`, `author`, `series`, or `genre` must be defined. - At least one of `title`, `author`, `series`, or `genre` must be defined.
- `priority` is the ELO ranking, initializing to `0`. - `priority` is the ELO ranking, initializing to `0`.
- `progress` represents read/unread/in-progress status. `0` is unread, `1` is - `progress` represents read/unread/in-progress status. `0` is unread, `1` is
read, and anything in-between in a percentage of progress. read, and anything in-between in a percentage of progress.
- `hidden` is undefined or `true` to hide a book from ALL of the interface - `hidden` is undefined or `true` to hide a book from ALL of the interface
without deleting its data. without deleting its data.
- `hours` is hours.minutes rather than the fractional part being percentage of
an hour.
Despite the script supporting otherwise, I highly recommend only using this for Despite the script supporting otherwise, I highly recommend only using this for
*specific book titles*. In my experience, using entries representing multiple *specific book titles*. In my experience, using entries representing multiple
+33 -84
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env luajit #!/usr/bin/env luajit
local filters = require "lib.filters"
local json = require "lib.dkjson" local json = require "lib.dkjson"
local sort_orders = require "lib.sort_orders"
math.randomseed(os.time()) math.randomseed(os.time())
local default_file = "books.json" local default_file = "books.json"
@@ -20,16 +22,23 @@ local load_json = function(file_name)
assert(file, error_message) assert(file, error_message)
local result = json.decode(file:read("*all")) local result = json.decode(file:read("*all"))
file:close() file:close()
result._loaded_file_location = file_name
return result return result
end end
local save_json = function(file_name, data) local save_json = function(data)
local file_name = data._loaded_file_location
data._loaded_file_location = nil
local file, error_message = io.open(file_name, "w") local file, error_message = io.open(file_name, "w")
assert(file, error_message) assert(file, error_message)
file:write(json.encode(data, { indent = true, })) file:write(json.encode(data, { indent = true, }))
file:write("\n") file:write("\n")
file:close() file:close()
debug("Saved.") debug("Saved.")
data._loaded_file_location = file_name
end end
local get_random_order = function(array) local get_random_order = function(array)
@@ -43,72 +52,6 @@ local get_random_order = function(array)
return order return order
end end
local filters = {
all_books = function(book)
if book.hidden then return false end
return true
end,
unread = function(book)
if book.hidden then return false end
if book.progress == 0 then
return true
end
return false
end,
read = function(book)
if book.hidden then return false end
if book.progress == 1 then
return true
end
return false
end,
in_progress = function(book)
if book.hidden then return false end
if book.progress > 0 and book.progress < 1 then
return true
end
return false
end,
exclude_series = function(book)
if book.hidden then return false end
if book.series and not (book.title or book.author) then
return false
end
return true
end,
}
local sort_orders
sort_orders = {
most_progress = function(A, B)
return A.progress > B.progress
end,
least_progress = function(A, B)
return A.progress < B.progress
end,
highest_priority = function(A, B)
return A.priority > B.priority
end,
lowest_priority = function(A, B)
return A.priority < B.priority
end,
most_pages_remaining = function(A, B, data)
if not (A.pages and B.pages) then -- if pages is unknown for both, fallback to percentage
return A.progress < B.progress
end
local average_page_count = data.total_pages / data.books_with_page_count
if not (average_page_count == average_page_count) then
average_page_count = 300 -- based on average novel length
end
local a = (A.pages or average_page_count) - (A.pages or average_page_count) * A.progress
local b = (B.pages or average_page_count) - (B.pages or average_page_count) * B.progress
return a > b
end,
fewest_pages_remaining = function(A, B, data)
return not sort_orders.most_pages_remaining(A, B, data)
end,
}
local get_display_name = function(book) local get_display_name = function(book)
local result = "" local result = ""
@@ -138,7 +81,10 @@ local get_display_name = function(book)
end end
if book.pages then if book.pages then
result = result .. " ~" .. math.floor(book.progress * book.pages) .. "/" .. book.pages .. " pages read" result = result .. " ~" .. math.floor(book.progress * book.pages) .. "/" .. book.pages .. " pages read (" .. math.floor(book.progress * 100) .. "%)"
elseif book.hours then
-- TODO correct for the fractional part being minutes instead of percent
result = result .. " ~" .. math.floor(book.progress * book.hours * 10) / 10 .. "/" .. book.hours .. " hours listened (" .. math.floor(book.progress * 100) .. "%)"
elseif book.progress > 0 and book.progress < 1 then elseif book.progress > 0 and book.progress < 1 then
result = result .. " ~" .. math.floor(book.progress * 100) .. "% read" result = result .. " ~" .. math.floor(book.progress * 100) .. "% read"
end end
@@ -340,13 +286,6 @@ end
local prep_sort_function = function(data, sort_function)
-- this allows me to pass data as a third argument for functions that use it
return function(A, B)
return sort_function(A, B, data)
end
end
local get_books_by_preset = function(data, preset_name) local get_books_by_preset = function(data, preset_name)
local selected_books = {} local selected_books = {}
for i = 1, #data.books do for i = 1, #data.books do
@@ -355,7 +294,7 @@ local get_books_by_preset = function(data, preset_name)
selected_books[#selected_books + 1] = book selected_books[#selected_books + 1] = book
end end
end end
table.sort(selected_books, prep_sort_function(sort_orders[data.defaults[preset_name].sort], data)) table.sort(selected_books, function(A, B) return sort_orders[data.defaults[preset_name].sort](A, B, data) end)
return selected_books return selected_books
end end
@@ -388,7 +327,7 @@ local print_list = function(data, options)
selected_books[#selected_books + 1] = book selected_books[#selected_books + 1] = book
end end
end end
table.sort(selected_books, prep_sort_function(sort_orders[options[2]], data)) table.sort(selected_books, function(A, B) return sort_orders[options[2]](A, B, data) end)
local limit = 10 local limit = 10
if options[3] == "all" then if options[3] == "all" then
@@ -410,8 +349,17 @@ local launch = function(file_name)
if path_exists(file_name) then if path_exists(file_name) then
data = load_json(file_name) data = load_json(file_name)
-- TODO verify data structure -- TODO verify data structure
local data_modified = false
if not data.books_with_page_count then if not data.books_with_page_count then
recalculate_total_pages(data) recalculate_total_pages(data)
data_modified = true
end
if not data.defaults.launch.max_displayed then
data.defaults.launch.max_displayed = 10
data_modified = true
end
if data_modified then
save_json(data)
end end
else else
data = { data = {
@@ -429,7 +377,7 @@ local launch = function(file_name)
}, },
}, },
} }
save_json(file_name, data) save_json(data)
end end
local selected_books = get_books_by_preset(data, "launch") local selected_books = get_books_by_preset(data, "launch")
@@ -438,11 +386,12 @@ local launch = function(file_name)
end end
local main = function(data, selected_books) local main = function(data, selected_books)
for i = 1, math.min(10, #selected_books) do local book_display_count = data.defaults.launch.max_displayed == 0 and #selected_books or data.defaults.launch.max_displayed
for i = 1, book_display_count do
local book = selected_books[i] local book = selected_books[i]
print(" " .. (i == 10 and "0" or i) .. ". " .. get_display_name(book)) print(" " .. i .. ". " .. get_display_name(book))
end end
print("Commands: " .. (#selected_books > 0 and "[0-9] to modify a book's progress. " or "") .. "[i <file>] to import from a JSON") print("Commands: " .. (#selected_books > 0 and "[#] to modify a book's progress. " or "") .. "[i <file>] to import from a JSON")
print(" file. [recalculate pages] Enter nothing to exit.") print(" file. [recalculate pages] Enter nothing to exit.")
print(" Adding/Selecting: Title OR Title by Author OR \"Name (type)\" for other types.") print(" Adding/Selecting: Title OR Title by Author OR \"Name (type)\" for other types.")
print(" Listing: list [filter] [sort] (all)") print(" Listing: list [filter] [sort] (all)")
@@ -453,7 +402,7 @@ local main = function(data, selected_books)
if #input == 0 then if #input == 0 then
os.exit(0) os.exit(0)
elseif numerical and (numerical >= 0 and numerical <= 9) then elseif numerical then
update_book(selected_books[numerical]) update_book(selected_books[numerical])
elseif input:sub(1, 1) == "i" then elseif input:sub(1, 1) == "i" then
import_json(data, input:sub(3)) import_json(data, input:sub(3))
@@ -470,11 +419,11 @@ local main = function(data, selected_books)
-- no? these are completely separate modes, there should be quick add, update progress, rank mode, add pages mode, etc -- no? these are completely separate modes, there should be quick add, update progress, rank mode, add pages mode, etc
end end
save_json(default_file, data) -- this assumes it is equivalent with how launch() is called save_json(data)
return true return true
end end
local data, selected_books = launch(default_file) -- the file name doesn't make it to main :\ local data, selected_books = launch(default_file)
while true do while true do
main(data, selected_books) main(data, selected_books)
end end
+43
View File
@@ -0,0 +1,43 @@
local filters = {
all_books = function(book)
if book.hidden then return false end
return true
end,
unread = function(book)
if book.hidden then return false end
if book.progress == 0 then
return true
end
return false
end,
read = function(book)
if book.hidden then return false end
if book.progress == 1 then
return true
end
return false
end,
in_progress = function(book)
if book.hidden then return false end
if book.progress > 0 and book.progress < 1 then
return true
end
return false
end,
exclude_series = function(book)
if book.hidden then return false end
if book.series and not (book.title or book.author) then
return false
end
return true
end,
audiobooks_only = function(book)
if book.hidden then return false end
if book.hours then
return true
end
return false
end,
}
return filters
+43
View File
@@ -0,0 +1,43 @@
local sort_orders
sort_orders = {
most_progress = function(A, B)
return A.progress > B.progress
end,
least_progress = function(A, B)
return A.progress < B.progress
end,
highest_priority = function(A, B)
return A.priority > B.priority
end,
lowest_priority = function(A, B)
return A.priority < B.priority
end,
most_pages_remaining = function(A, B, data)
if not (A.pages and B.pages) then -- if pages is unknown for both, fallback to percentage
return A.progress < B.progress
end
local average_page_count = data.total_pages / data.books_with_page_count
if not (average_page_count == average_page_count) then
average_page_count = 300 -- based on average novel length
end
local a = (A.pages or average_page_count) - (A.pages or average_page_count) * A.progress
local b = (B.pages or average_page_count) - (B.pages or average_page_count) * B.progress
return a > b
end,
fewest_pages_remaining = function(A, B, data)
return not sort_orders.most_pages_remaining(A, B, data)
end,
most_time_remaining = function(A, B)
if not (A.hours and B.hours) then -- fallback to percentage
return A.progress < B.progress
end
local a = A.hours - A.hours * A.progress
local b = B.hours - B.hours * B.progress
return a > b
end,
least_time_remaining = function(A, B)
return not sort_orders.most_time_remaining(A, B)
end,
}
return sort_orders