mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
[Plugin] Joplin support (#5431)
Adds joplin support, fixes https://github.com/koreader/koreader/issues/5086 Changes -adds a submenu to evernote menu - -Joplin |-Set IP and port |-Set authorization token |-Export to Joplin |-Help -adds EvernoteExporter:exportBooknotesToJoplin() -adds JoplinClient.lua -modifies html_export, txt_export and joplin_export flags to work with each other. (eg if user selects one others deactivated)
This commit is contained in:
committed by
Frans de Jonge
parent
3e3b00429f
commit
a0e2f02c32
136
plugins/evernote.koplugin/JoplinClient.lua
Normal file
136
plugins/evernote.koplugin/JoplinClient.lua
Normal file
@@ -0,0 +1,136 @@
|
||||
local json = require("json")
|
||||
local http = require("socket.http")
|
||||
local ltn12 = require("ltn12")
|
||||
|
||||
local JoplinClient = {
|
||||
server_ip = "localhost",
|
||||
server_port = 41184,
|
||||
auth_token = ""
|
||||
}
|
||||
|
||||
function JoplinClient:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
return o
|
||||
end
|
||||
|
||||
function JoplinClient:_makeRequest(url, method, request_body)
|
||||
local sink = {}
|
||||
local request_body_json = json.encode(request_body)
|
||||
local source = ltn12.source.string(request_body_json)
|
||||
http.request{
|
||||
url = url,
|
||||
method = method,
|
||||
sink = ltn12.sink.table(sink),
|
||||
source = source,
|
||||
headers = {
|
||||
["Content-Length"] = #request_body_json,
|
||||
["Content-Type"] = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
if not sink[1] then
|
||||
error("No response from Joplin Server")
|
||||
end
|
||||
|
||||
local response = json.decode(sink[1])
|
||||
|
||||
if response.error then
|
||||
error(response.error)
|
||||
end
|
||||
|
||||
return response
|
||||
end
|
||||
|
||||
function JoplinClient:ping()
|
||||
local sink = {}
|
||||
|
||||
http.request{
|
||||
url = "http://"..self.server_ip..":"..self.server_port.."/ping",
|
||||
method = "GET",
|
||||
sink = ltn12.sink.table(sink)
|
||||
}
|
||||
|
||||
if sink[1] == "JoplinClipperServer" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- If successful returns id of found note.
|
||||
function JoplinClient:findNoteByTitle(title, notebook_id)
|
||||
local url = "http://"..self.server_ip..":"..self.server_port.."/notes?".."token="..self.auth_token.."&fields=id,title,parent_id"
|
||||
|
||||
local notes = self:_makeRequest(url, "GET")
|
||||
|
||||
for _, note in pairs(notes) do
|
||||
if note.title == title then
|
||||
if notebook_id == nil or note.parent_id == notebook_id then
|
||||
return note.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
|
||||
end
|
||||
|
||||
-- If successful returns id of found notebook (folder).
|
||||
function JoplinClient:findNotebookByTitle(title)
|
||||
local url = "http://"..self.server_ip..":"..self.server_port.."/folders?".."token="..self.auth_token.."&".."query="..title
|
||||
|
||||
local folders = self:_makeRequest(url, "GET")
|
||||
|
||||
for _, folder in pairs(folders) do
|
||||
if folder.title== title then
|
||||
return folder.id
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- If successful returns id of created notebook (folder).
|
||||
function JoplinClient:createNotebook(title, created_time)
|
||||
local request_body = {
|
||||
title = title,
|
||||
created_time = created_time
|
||||
}
|
||||
|
||||
local url = "http://"..self.server_ip..":"..self.server_port.."/folders?".."token="..self.auth_token
|
||||
local response = self:_makeRequest(url, "POST", request_body)
|
||||
|
||||
return response.id
|
||||
end
|
||||
|
||||
|
||||
-- If successful returns id of created note.
|
||||
function JoplinClient:createNote(title, note, parent_id, created_time)
|
||||
local request_body = {
|
||||
title = title,
|
||||
body = note,
|
||||
parent_id = parent_id,
|
||||
created_time = created_time
|
||||
}
|
||||
local url = "http://"..self.server_ip..":"..self.server_port.."/notes?".."token="..self.auth_token
|
||||
local response = self:_makeRequest(url, "POST", request_body)
|
||||
|
||||
return response.id
|
||||
end
|
||||
|
||||
-- If successful returns id of updated note.
|
||||
function JoplinClient:updateNote(note_id, note, title, parent_id)
|
||||
local request_body = {
|
||||
body = note,
|
||||
title = title,
|
||||
parent_id = parent_id
|
||||
}
|
||||
|
||||
local url = "http://"..self.server_ip..":"..self.server_port.."/notes/"..note_id.."?token="..self.auth_token
|
||||
local response = self:_makeRequest(url, "PUT", request_body)
|
||||
return response.id
|
||||
end
|
||||
|
||||
return JoplinClient
|
||||
@@ -10,6 +10,7 @@ local Screen = require("device").screen
|
||||
local util = require("ffi/util")
|
||||
local Device = require("device")
|
||||
local DEBUG = require("dbg")
|
||||
local JoplinClient = require("JoplinClient")
|
||||
local T = require("ffi/util").template
|
||||
local _ = require("gettext")
|
||||
local N_ = _.ngettext
|
||||
@@ -36,11 +37,21 @@ function EvernoteExporter:init()
|
||||
self.evernote_username = settings.username or ""
|
||||
self.evernote_token = settings.token
|
||||
self.notebook_guid = settings.notebook
|
||||
self.joplin_IP = settings.joplin_IP or "localhost"
|
||||
self.joplin_port = settings.joplin_port or 41185
|
||||
self.joplin_token = settings.joplin_token -- or your token
|
||||
self.joplin_notebook_guid = settings.joplin_notebook_guid or nil
|
||||
self.html_export = settings.html_export or false
|
||||
self.joplin_export = settings.joplin_export or false
|
||||
self.txt_export = settings.txt_export or false
|
||||
--- @todo Is this if block necessarry? Nowhere in the code they are assigned both true.
|
||||
-- Do they check against external modifications to settings file?
|
||||
|
||||
if self.html_export then
|
||||
self.txt_export = false
|
||||
else
|
||||
self.txt_export = settings.txt_export or false
|
||||
self.joplin_export = false
|
||||
elseif self.txt_export then
|
||||
self.joplin_export = false
|
||||
end
|
||||
|
||||
self.parser = MyClipping:new{
|
||||
@@ -59,7 +70,10 @@ function EvernoteExporter:isDocless()
|
||||
end
|
||||
|
||||
function EvernoteExporter:readyToExport()
|
||||
return self.evernote_token ~= nil or self.html_export ~= false or self.txt_export ~= false
|
||||
return self.evernote_token ~= nil or
|
||||
self.html_export ~= false or
|
||||
self.txt_export ~= false or
|
||||
self.joplin_export ~= false
|
||||
end
|
||||
|
||||
function EvernoteExporter:migrateClippings()
|
||||
@@ -111,6 +125,129 @@ function EvernoteExporter:addToMainMenu(menu_items)
|
||||
} or nil
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Joplin") ,
|
||||
checked_func = function() return self.joplin_export end,
|
||||
sub_item_table ={
|
||||
{
|
||||
text = _("Set Joplin IP and Port"),
|
||||
keep_menu_open = true,
|
||||
callback = function()
|
||||
local MultiInputDialog = require("ui/widget/multiinputdialog")
|
||||
local url_dialog
|
||||
url_dialog = MultiInputDialog:new{
|
||||
title = _("Set Joplin IP and port number"),
|
||||
fields = {
|
||||
{
|
||||
text = self.joplin_IP,
|
||||
input_type = "string"
|
||||
},
|
||||
{
|
||||
text = self.joplin_port,
|
||||
input_type = "number"
|
||||
}
|
||||
},
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
callback = function()
|
||||
UIManager:close(url_dialog)
|
||||
end
|
||||
},
|
||||
{
|
||||
text = _("OK"),
|
||||
callback = function()
|
||||
local fields = url_dialog:getFields()
|
||||
local ip = fields[1]
|
||||
local port = tonumber(fields[2])
|
||||
if ip ~= "" then
|
||||
if port and port < 65355 then
|
||||
self.joplin_IP = ip
|
||||
self.joplin_port = port
|
||||
end
|
||||
self:saveSettings()
|
||||
end
|
||||
UIManager:close(url_dialog)
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UIManager:show(url_dialog)
|
||||
url_dialog:onShowKeyboard()
|
||||
end
|
||||
},
|
||||
{
|
||||
text = _("Set authorization token"),
|
||||
keep_menu_open = true,
|
||||
callback = function()
|
||||
local MultiInputDialog = require("ui/widget/multiinputdialog")
|
||||
local auth_dialog
|
||||
auth_dialog = MultiInputDialog:new{
|
||||
title = _("Set authorization token for Joplin"),
|
||||
fields = {
|
||||
{
|
||||
text = self.joplin_token,
|
||||
input_type = "string"
|
||||
}
|
||||
},
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
callback = function()
|
||||
UIManager:close(auth_dialog)
|
||||
end
|
||||
},
|
||||
{
|
||||
text = _("Set token"),
|
||||
callback = function()
|
||||
local auth_field = auth_dialog:getFields()
|
||||
self.joplin_token = auth_field[1]
|
||||
self:saveSettings()
|
||||
UIManager:close(auth_dialog)
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UIManager:show(auth_dialog)
|
||||
auth_dialog:onShowKeyboard()
|
||||
end
|
||||
},
|
||||
{
|
||||
text = _("Export to Joplin"),
|
||||
checked_func = function() return self.joplin_export end,
|
||||
callback = function()
|
||||
self.joplin_export = not self.joplin_export
|
||||
if self.joplin_export then
|
||||
self.html_export = false
|
||||
self.txt_export = false
|
||||
end
|
||||
self:saveSettings()
|
||||
end
|
||||
},
|
||||
{
|
||||
text = _("Help"),
|
||||
keep_menu_open = true,
|
||||
callback = function()
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = T(_([[You can enter your auth token on your computer by saving an empty token. Then quit KOReader, edit the evernote.joplin_token field in %1/settings.reader.lua after creating a backup, and restart KOReader once you're done.
|
||||
|
||||
To export to Joplin, you must forward the IP and port used by this plugin to the localhost:port on which Joplin is listening. This can be done with socat or a similar program. For example:
|
||||
|
||||
For Windows: netsh interface portproxy add listeningaddress:0.0.0.0 listeningport:41185 connectaddress:localhost connectport:41184
|
||||
|
||||
For Linux: $socat tcp-listen:41185,reuseaddr,fork tcp:localhost:41184
|
||||
|
||||
For more information, please visit https://github.com/koreader/koreader/wiki/Evernote-export.]])
|
||||
,DataStorage:getDataDir())
|
||||
})
|
||||
end
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
text = _("Export all notes in this book"),
|
||||
enabled_func = function()
|
||||
@@ -148,7 +285,10 @@ function EvernoteExporter:addToMainMenu(menu_items)
|
||||
checked_func = function() return self.html_export end,
|
||||
callback = function()
|
||||
self.html_export = not self.html_export
|
||||
if self.html_export then self.txt_export = false end
|
||||
if self.html_export then
|
||||
self.txt_export = false
|
||||
self.joplin_export = false
|
||||
end
|
||||
self:saveSettings()
|
||||
end
|
||||
},
|
||||
@@ -157,7 +297,10 @@ function EvernoteExporter:addToMainMenu(menu_items)
|
||||
checked_func = function() return self.txt_export end,
|
||||
callback = function()
|
||||
self.txt_export = not self.txt_export
|
||||
if self.txt_export then self.html_export = false end
|
||||
if self.txt_export then
|
||||
self.html_export = false
|
||||
self.joplin_export = false
|
||||
end
|
||||
self:saveSettings()
|
||||
end
|
||||
},
|
||||
@@ -288,6 +431,11 @@ function EvernoteExporter:saveSettings()
|
||||
notebook = self.notebook_guid,
|
||||
html_export = self.html_export,
|
||||
txt_export = self.txt_export,
|
||||
joplin_IP = self.joplin_IP,
|
||||
joplin_port = self.joplin_port,
|
||||
joplin_token = self.joplin_token,
|
||||
joplin_notebook_guid = self.joplin_notebook_guid,
|
||||
joplin_export = self.joplin_export
|
||||
}
|
||||
G_reader_settings:saveSetting("evernote", settings)
|
||||
end
|
||||
@@ -358,7 +506,8 @@ end
|
||||
function EvernoteExporter:exportClippings(clippings)
|
||||
local client = nil
|
||||
local exported_stamp
|
||||
if not self.html_export and not self.txt_export then
|
||||
local joplin_client
|
||||
if not (self.html_export or self.txt_export or self.joplin_export) then
|
||||
client = require("EvernoteClient"):new{
|
||||
domain = self.evernote_domain,
|
||||
authToken = self.evernote_token,
|
||||
@@ -368,6 +517,19 @@ function EvernoteExporter:exportClippings(clippings)
|
||||
exported_stamp= "html"
|
||||
elseif self.txt_export then
|
||||
exported_stamp = "txt"
|
||||
elseif self.joplin_export then
|
||||
exported_stamp = "joplin"
|
||||
joplin_client = JoplinClient:new{
|
||||
server_ip = self.joplin_IP,
|
||||
server_port = self.joplin_port,
|
||||
auth_token = self.joplin_token
|
||||
}
|
||||
---@todo Check if user deleted our notebook, in that case note
|
||||
-- will end up in random folder in Joplin.
|
||||
if not self.joplin_notebook_guid then
|
||||
self.joplin_notebook_guid = joplin_client:createNotebook(self.notebook_name)
|
||||
self:saveSettings()
|
||||
end
|
||||
else
|
||||
assert("an exported_stamp is expected for a new export type")
|
||||
end
|
||||
@@ -386,6 +548,8 @@ function EvernoteExporter:exportClippings(clippings)
|
||||
ok, err = pcall(self.exportBooknotesToHTML, self, title, booknotes)
|
||||
elseif self.txt_export then
|
||||
ok, err = pcall(self.exportBooknotesToTXT, self, title, booknotes)
|
||||
elseif self.joplin_export then
|
||||
ok, err = pcall(self.exportBooknotesToJoplin, self, joplin_client, title, booknotes)
|
||||
else
|
||||
ok, err = pcall(self.exportBooknotesToEvernote, self, client, title, booknotes)
|
||||
end
|
||||
@@ -507,4 +671,30 @@ function EvernoteExporter:exportBooknotesToTXT(title, booknotes)
|
||||
end
|
||||
end
|
||||
|
||||
function EvernoteExporter:exportBooknotesToJoplin(client, title, booknotes)
|
||||
if not client:ping() then
|
||||
error("Cannot reach Joplin server")
|
||||
end
|
||||
|
||||
local note_guid = client:findNoteByTitle(title, self.joplin_notebook_guid)
|
||||
local note = ""
|
||||
for _, chapter in ipairs(booknotes) do
|
||||
if chapter.title then
|
||||
note = note .. "\n\t*" .. chapter.title .. "*\n\n * * *"
|
||||
end
|
||||
|
||||
for _, clipping in ipairs(chapter) do
|
||||
note = note .. os.date("%Y-%m-%d %H:%M:%S \n", clipping.time)
|
||||
note = note .. clipping.text .. "\n * * *\n"
|
||||
end
|
||||
end
|
||||
|
||||
if note_guid then
|
||||
client:updateNote(note_guid, note)
|
||||
else
|
||||
client:createNote(title, note, self.joplin_notebook_guid)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return EvernoteExporter
|
||||
|
||||
Reference in New Issue
Block a user