From cff267f2019fa261080c3b46b0bffac47bba061d Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 29 Apr 2026 14:22:20 +0200 Subject: [PATCH] feat: sync book covers from KOReader plugin - Import response adds missing_cover_md5; POST /api/plugin/books/:md5/cover (multipart) - CoversService.filterMd5WithoutCover; BooksRepository.getByMd5; plugin version 0.3.1 - Plugin: ReadHistory + partialMD5 path map, cover extract, multipart upload after import - Tests, README and CHANGELOG --- CHANGELOG.md | 4 +- README.md | 2 + apps/server/src/books/books-repository.ts | 4 + .../server/src/books/covers/covers-service.ts | 19 +- .../src/koplugin/koplugin-router.test.ts | 176 +++++++++++++++++- apps/server/src/koplugin/koplugin-router.ts | 97 +++++++++- package-lock.json | 6 +- plugins/koinsight.koplugin/call_multipart.lua | 96 ++++++++++ plugins/koinsight.koplugin/const.lua | 2 +- plugins/koinsight.koplugin/cover_extract.lua | 85 +++++++++ .../cover_path_resolver.lua | 28 +++ plugins/koinsight.koplugin/cover_sync.lua | 52 ++++++ plugins/koinsight.koplugin/upload.lua | 11 +- 13 files changed, 570 insertions(+), 12 deletions(-) create mode 100644 plugins/koinsight.koplugin/call_multipart.lua create mode 100644 plugins/koinsight.koplugin/cover_extract.lua create mode 100644 plugins/koinsight.koplugin/cover_path_resolver.lua create mode 100644 plugins/koinsight.koplugin/cover_sync.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 8886bdd2..64b1dd9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,9 @@ New syncs won't create duplicates. If you still see duplicates, you most likely ### Breaking Changes -Plugin version 0.3.0 required. Update it before syncing. +Plugin version 0.3.1 required. Update it before syncing. + +KOReader cover sync: after import, the server returns `missing_cover_md5` and the plugin uploads PNG covers for those books when it can resolve a filepath via reading history. --- diff --git a/README.md b/README.md index 9a2bb915..a1a32795 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,8 @@ The KoInsight plugin syncs your reading statistics from KOReader to KoInsight. - ⚠️ Make sure your KOReader device has network access to the server. 1. Click **Sync** in the KoInsight plugin menu. +After a successful sync, the server may ask the plugin to upload **book covers** that KoInsight does not have yet. The plugin then looks up each book’s file path from **KOReader reading history**, extracts a cover (from the CoverBrowser cache when available, otherwise by opening the document), and sends it to the server. Books that never appear in history cannot be matched to a file on disk, so open them at least once before syncing if you want their covers uploaded. + Reload the KoInsight web dashboard. If everything went well (🤞), your data should appear. ### Manual Upload: `statistics.sqlite` diff --git a/apps/server/src/books/books-repository.ts b/apps/server/src/books/books-repository.ts index 45fdfbec..d4c13ecb 100644 --- a/apps/server/src/books/books-repository.ts +++ b/apps/server/src/books/books-repository.ts @@ -18,6 +18,10 @@ export class BooksRepository { return db('book').where({ id }).first(); } + static async getByMd5(md5: Book['md5']): Promise { + return db('book').where({ md5 }).first(); + } + static async insert(book: Partial): Promise { return db('book').insert(book); } diff --git a/apps/server/src/books/covers/covers-service.ts b/apps/server/src/books/covers/covers-service.ts index 0862361e..11f7f642 100644 --- a/apps/server/src/books/covers/covers-service.ts +++ b/apps/server/src/books/covers/covers-service.ts @@ -1,9 +1,24 @@ import { Book } from '@koinsight/common/types'; -import { existsSync, mkdirSync, promises, rename, rmSync } from 'fs'; +import { existsSync, mkdirSync, promises, rmSync } from 'fs'; import path from 'path'; import { appConfig } from '../../config'; export class CoversService { + /** Subset of md5 values that have no cover file on disk (single directory read). */ + static async filterMd5WithoutCover(md5s: string[]): Promise { + if (md5s.length === 0) { + return []; + } + let files: string[]; + try { + files = await promises.readdir(appConfig.coversPath); + } catch { + return [...new Set(md5s)]; + } + const unique = [...new Set(md5s)]; + return unique.filter((md5) => !files.some((f) => f.startsWith(md5))); + } + static async get(book: Book): Promise { const files = await promises.readdir(appConfig.coversPath); const file = files.find((f) => f.startsWith(book.md5)); @@ -33,6 +48,6 @@ export class CoversService { const extension = path.extname(file.originalname) || ''; const newFilename = `${book.md5}${extension}`; const newPath = path.join(path.dirname(file.path), newFilename); - await rename(file.path, newPath, () => {}); + await promises.rename(file.path, newPath); } } diff --git a/apps/server/src/koplugin/koplugin-router.test.ts b/apps/server/src/koplugin/koplugin-router.test.ts index 03ec6fc5..9d78708b 100644 --- a/apps/server/src/koplugin/koplugin-router.test.ts +++ b/apps/server/src/koplugin/koplugin-router.test.ts @@ -1,5 +1,8 @@ import express from 'express'; +import { mkdirSync, writeFileSync } from 'fs'; +import path from 'path'; import request from 'supertest'; +import { appConfig } from '../config'; import { createDevice } from '../db/factories/device-factory'; import { fakeKoReaderAnnotation } from '../db/factories/koreader-annotation-factory'; import { db } from '../knex'; @@ -100,7 +103,8 @@ describe('koplugin-router', () => { }); expect(response.status).toBe(200); - expect(response.body).toEqual({ message: 'Upload successful' }); + expect(response.body.message).toBe('Upload successful'); + expect(response.body.missing_cover_md5).toContain(bookMd5); const book = await db('book').where({ md5: bookMd5 }).first(); expect(book).toEqual( @@ -172,7 +176,8 @@ describe('koplugin-router', () => { }); expect(response.status).toBe(200); - expect(response.body).toEqual({ message: 'Upload successful' }); + expect(response.body.message).toBe('Upload successful'); + expect(response.body.missing_cover_md5).toContain(bookMd5); const book = await db('book').where({ md5: bookMd5 }).first(); expect(book).toEqual( @@ -214,6 +219,173 @@ describe('koplugin-router', () => { expect(response.status).toBe(400); expect(response.body.error).toContain('Unsupported plugin version'); }); + + it('omits md5 from missing_cover_md5 when a cover file already exists', async () => { + const bookMd5 = 'cover_exists_md5_12'; + const device = await createDevice(db); + + mkdirSync(appConfig.coversPath, { recursive: true }); + writeFileSync(path.join(appConfig.coversPath, `${bookMd5}.jpg`), Buffer.from([0xff, 0xd8, 0xff])); + + const response = await request(app) + .post('/koplugin/import') + .send({ + version: REQUIRED_PLUGIN_VERSION, + books: [ + { + md5: bookMd5, + title: 'Has Cover On Disk', + authors: 'Author', + language: 'en', + pages: 50, + total_read_time: 0, + total_read_pages: 0, + }, + ], + stats: [ + { + book_md5: bookMd5, + device_id: device.id, + start_time: 3000, + duration: 30, + page: 1, + total_pages: 50, + }, + ], + annotations: {}, + }); + + expect(response.status).toBe(200); + expect(response.body.missing_cover_md5).not.toContain(bookMd5); + }); + }); + + describe('POST /koplugin/books/:md5/cover', () => { + it('uploads a cover image for a book', async () => { + const bookMd5 = 'plugin_cover_upload_md5'; + const device = await createDevice(db); + + await request(app) + .post('/koplugin/import') + .send({ + version: REQUIRED_PLUGIN_VERSION, + books: [ + { + md5: bookMd5, + title: 'Cover Upload Book', + authors: 'Author', + language: 'en', + pages: 10, + total_read_time: 0, + total_read_pages: 0, + }, + ], + stats: [ + { + book_md5: bookMd5, + device_id: device.id, + start_time: 4000, + duration: 10, + page: 1, + total_pages: 10, + }, + ], + annotations: {}, + }); + + const pngBytes = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'base64' + ); + + const response = await request(app) + .post(`/koplugin/books/${encodeURIComponent(bookMd5)}/cover`) + .field('version', REQUIRED_PLUGIN_VERSION) + .attach('file', pngBytes, 'cover.png'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ message: 'Cover updated' }); + + mkdirSync(appConfig.coversPath, { recursive: true }); + const importAgain = await request(app) + .post('/koplugin/import') + .send({ + version: REQUIRED_PLUGIN_VERSION, + books: [ + { + md5: bookMd5, + title: 'Cover Upload Book', + authors: 'Author', + language: 'en', + pages: 10, + total_read_time: 0, + total_read_pages: 0, + }, + ], + stats: [], + annotations: {}, + }); + expect(importAgain.body.missing_cover_md5).not.toContain(bookMd5); + }); + + it('returns 404 when book md5 is unknown', async () => { + const pngBytes = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'base64' + ); + + const response = await request(app) + .post('/koplugin/books/unknown_md5_xyz/cover') + .field('version', REQUIRED_PLUGIN_VERSION) + .attach('file', pngBytes, 'cover.png'); + + expect(response.status).toBe(404); + }); + + it('returns 400 when plugin version is wrong', async () => { + const bookMd5 = 'version_check_cover_md5'; + const device = await createDevice(db); + + await request(app) + .post('/koplugin/import') + .send({ + version: REQUIRED_PLUGIN_VERSION, + books: [ + { + md5: bookMd5, + title: 'V Book', + authors: 'A', + language: 'en', + pages: 5, + total_read_time: 0, + total_read_pages: 0, + }, + ], + stats: [ + { + book_md5: bookMd5, + device_id: device.id, + start_time: 5000, + duration: 5, + page: 1, + total_pages: 5, + }, + ], + annotations: {}, + }); + + const pngBytes = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'base64' + ); + + const response = await request(app) + .post(`/koplugin/books/${bookMd5}/cover`) + .field('version', '0.1.0') + .attach('file', pngBytes, 'cover.png'); + + expect(response.status).toBe(400); + }); }); describe('GET /koplugin/health', () => { diff --git a/apps/server/src/koplugin/koplugin-router.ts b/apps/server/src/koplugin/koplugin-router.ts index 10bef0b0..affe2b1b 100644 --- a/apps/server/src/koplugin/koplugin-router.ts +++ b/apps/server/src/koplugin/koplugin-router.ts @@ -4,14 +4,19 @@ import { Device } from '@koinsight/common/types/device'; import { PageStat } from '@koinsight/common/types/page-stat'; import archiver from 'archiver'; import { NextFunction, Request, Response, Router } from 'express'; +import { existsSync, mkdirSync, unlink } from 'fs'; +import multer from 'multer'; import path from 'path'; +import { BooksRepository } from '../books/books-repository'; +import { CoversService } from '../books/covers/covers-service'; +import { appConfig } from '../config'; import { DeviceRepository } from '../devices/device-repository'; import { UploadService } from '../upload/upload-service'; // Router for KoInsight koreader plugin const router = Router(); -export const REQUIRED_PLUGIN_VERSION = '0.3.0'; +export const REQUIRED_PLUGIN_VERSION = '0.3.1'; const rejectOldPluginVersion = (req: Request, res: Response, next: NextFunction) => { const { version } = req.body; @@ -26,6 +31,25 @@ const rejectOldPluginVersion = (req: Request, res: Response, next: NextFunction) next(); }; +const pluginCoverUpload = multer({ + dest: appConfig.coversPath, + fileFilter: (_req, file, cb) => { + const allowedExtensions = ['.png', '.jpg', '.jpeg', '.gif']; + if ( + file.mimetype === 'application/octet-stream' || + allowedExtensions.some((ext) => file.originalname.toLowerCase().endsWith(ext)) + ) { + cb(null, true); + } else { + cb(new Error(`Only ${allowedExtensions.join(', ')} files are allowed`)); + } + }, + limits: { fileSize: 10 * 1024 * 1024 }, +}).fields([ + { name: 'file', maxCount: 1 }, + { name: 'version', maxCount: 1 }, +]); + router.post('/device', rejectOldPluginVersion, async (req, res) => { const { id, model } = req.body; @@ -65,13 +89,82 @@ router.post('/import', rejectOldPluginVersion, async (req, res) => { ); await UploadService.uploadStatisticData(koreaderBooks, newPageStats, annotations, deviceId); - res.status(200).json({ message: 'Upload successful' }); + + const bookList = Array.isArray(koreaderBooks) ? koreaderBooks : []; + const importedMd5s = bookList.map((b) => b.md5).filter(Boolean) as string[]; + const missing_cover_md5 = await CoversService.filterMd5WithoutCover(importedMd5s); + + res.status(200).json({ + message: 'Upload successful', + missing_cover_md5, + }); } catch (err) { console.error(err); res.status(500).json({ error: 'Error importing data' }); } }); +router.post('/books/:md5/cover', (req, res, next) => { + if (!existsSync(appConfig.coversPath)) { + mkdirSync(appConfig.coversPath, { recursive: true }); + } + next(); +}, (req, res, next) => { + pluginCoverUpload(req, res, (err) => { + if (err) { + console.error('Cover upload parse error:', err); + res.status(400).json({ error: err instanceof Error ? err.message : 'Invalid upload' }); + return; + } + next(); + }); +}, async (req: Request, res: Response) => { + const files = req.files as { file?: Express.Multer.File[] }; + const file = files?.file?.[0]; + const version = req.body?.version; + + const cleanupTemp = () => { + if (file?.path) { + try { + unlink(file.path, () => {}); + } catch { + // ignore + } + } + }; + + if (!version || version !== REQUIRED_PLUGIN_VERSION) { + cleanupTemp(); + res.status(400).json({ + error: `Unsupported plugin version. Version must be ${REQUIRED_PLUGIN_VERSION}. Please update your KOReader koinsight.koplugin`, + }); + return; + } + + if (!file) { + res.status(400).json({ error: 'Missing file upload' }); + return; + } + + const md5 = req.params.md5; + try { + const book = await BooksRepository.getByMd5(md5); + if (!book) { + cleanupTemp(); + res.status(404).json({ error: 'Book not found' }); + return; + } + + await CoversService.deleteExisting(book); + await CoversService.upload(book, file); + res.status(200).json({ message: 'Cover updated' }); + } catch (e) { + cleanupTemp(); + console.error('Error uploading plugin cover:', e); + res.status(500).json({ error: 'Unable to update cover' }); + } +}); + // TODO: implement check in koreader plugin router.get('/health', rejectOldPluginVersion, async (_, res) => { res.status(200).json({ message: 'Plugin is healthy' }); diff --git a/package-lock.json b/package-lock.json index f1f50549..ddedf278 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ } }, "apps/server": { - "version": "v0.2.0", + "version": "v0.2.2", "license": "MIT", "dependencies": { "@koinsight/common": "*", @@ -362,7 +362,7 @@ } }, "apps/web": { - "version": "v0.2.0", + "version": "v0.2.2", "license": "MIT", "dependencies": { "@koinsight/common": "*", @@ -9209,7 +9209,7 @@ }, "packages/common": { "name": "@koinsight/common", - "version": "v0.2.0" + "version": "v0.2.2" } } } diff --git a/plugins/koinsight.koplugin/call_multipart.lua b/plugins/koinsight.koplugin/call_multipart.lua new file mode 100644 index 00000000..bdb13ba4 --- /dev/null +++ b/plugins/koinsight.koplugin/call_multipart.lua @@ -0,0 +1,96 @@ +local JSON = require("json") +local logger = require("logger") +local ltn12 = require("ltn12") +local socket = require("socket") +local http = require("socket.http") +local socketutil = require("socketutil") +local UIManager = require("ui/uimanager") +local InfoMessage = require("ui/widget/infomessage") +local _ = require("gettext") + +local function build_multipart_body(boundary, fields, file_content, file_field, filename, mime) + local parts = {} + for k, v in pairs(fields) do + table.insert(parts, "--" .. boundary .. "\r\n") + table.insert( + parts, + string.format('Content-Disposition: form-data; name="%s"\r\n\r\n%s\r\n', k, tostring(v)) + ) + end + table.insert(parts, "--" .. boundary .. "\r\n") + table.insert( + parts, + string.format( + 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n', + file_field, + filename + ) + ) + table.insert(parts, "Content-Type: " .. mime .. "\r\n\r\n") + table.insert(parts, file_content) + table.insert(parts, "\r\n--" .. boundary .. "--\r\n") + return table.concat(parts) +end + +--- POST multipart/form-data; returns same shape as call_api: ok, result_or_key [, http_code] +return function(url, fields, filepath, file_field_name, filename, mime, quiet) + quiet = quiet or false + file_field_name = file_field_name or "file" + filename = filename or "cover.png" + mime = mime or "image/png" + + local fh, data = io.open(filepath, "rb"), nil + if not fh then + logger.err("[KoInsight] callMultipart: cannot read file", filepath) + return false, "file_read_error" + end + data = fh:read("*a") + fh:close() + + local boundary = "----KoInsightFormBoundary" .. tostring(os.time()) + local body = build_multipart_body(boundary, fields, data, file_field_name, filename, mime) + + local sink = {} + local request = { + method = "POST", + url = url, + headers = { + ["Content-Type"] = "multipart/form-data; boundary=" .. boundary, + ["Content-Length"] = tostring(#body), + }, + source = ltn12.source.string(body), + sink = ltn12.sink.table(sink), + } + + logger.dbg("[KoInsight] callMultipart:", request.method, request.url) + socketutil:set_timeout(socketutil.LARGE_BLOCK_TIMEOUT, socketutil.LARGE_TOTAL_TIMEOUT) + local code, resp_headers, status = socket.skip(1, http.request(request)) + socketutil:reset_timeout() + + if resp_headers == nil then + logger.err("[KoInsight] callMultipart: network error", status or code) + return false, "network_error" + end + + if code == 200 then + local content = table.concat(sink) + if content == nil or content == "" or string.sub(content, 1, 1) ~= "{" then + logger.err("[KoInsight] callMultipart: invalid JSON response", content) + return false, "empty_response" + end + local ok, result = pcall(JSON.decode, content) + if ok and result then + return true, result + end + logger.err("[KoInsight] callMultipart: JSON decode failed", content) + return false, "invalid_response" + end + + if not quiet then + logger.err("[KoInsight] callMultipart: HTTP error", status or code) + UIManager:show(InfoMessage:new({ + text = _("Could not upload cover."), + })) + end + return false, "http_error", code +end diff --git a/plugins/koinsight.koplugin/const.lua b/plugins/koinsight.koplugin/const.lua index 6c83f8a8..4c2eb957 100644 --- a/plugins/koinsight.koplugin/const.lua +++ b/plugins/koinsight.koplugin/const.lua @@ -1,5 +1,5 @@ local const = {} -const.VERSION = "0.3.0" +const.VERSION = "0.3.1" return const diff --git a/plugins/koinsight.koplugin/cover_extract.lua b/plugins/koinsight.koplugin/cover_extract.lua new file mode 100644 index 00000000..284578d6 --- /dev/null +++ b/plugins/koinsight.koplugin/cover_extract.lua @@ -0,0 +1,85 @@ +local DataStorage = require("datastorage") +local DocSettings = require("docsettings") +local DocumentRegistry = require("document/documentregistry") +local logger = require("logger") +local lfs = require("libs/libkoreader-lfs") + +local M = {} + +local function ensure_cache_dir() + local dir = DataStorage:getDataDir() .. "/cache" + if lfs.attributes(dir, "mode") ~= "directory" then + lfs.mkdir(dir) + end + return dir +end + +--- Try CoverBrowser bookinfo cache (if module is available). +local function cover_bb_from_bookinfo(filepath) + local ok, BookInfoManager = pcall(require, "bookinfomanager") + if not ok or not BookInfoManager or not BookInfoManager.getBookInfo then + return nil + end + local bookinfo = BookInfoManager:getBookInfo(filepath, true) + if bookinfo and bookinfo.has_cover and bookinfo.cover_bb then + return bookinfo.cover_bb + end + return nil +end + +--- Open document and read cover page (same strategy as FileManagerBookInfo:getCoverImage, without custom UI). +local function cover_bb_from_document(filepath) + local custom_cover = DocSettings:findCustomCoverFile(filepath) + if custom_cover then + local cover_doc = DocumentRegistry:openDocument(custom_cover) + if cover_doc then + local bb = cover_doc:getCoverPageImage() + cover_doc:close() + if bb then + return bb + end + end + end + + local doc = DocumentRegistry:openDocument(filepath) + if not doc then + return nil + end + if doc.loadDocument then + doc:loadDocument(false) + end + local bb = doc:getCoverPageImage() + doc:close() + return bb +end + +--- Extract cover to a temporary PNG under koreader/cache; caller should os.remove when done. +function M.extractCoverToTempPng(filepath) + if not filepath then + return nil + end + + local bb = cover_bb_from_bookinfo(filepath) + if not bb then + bb = cover_bb_from_document(filepath) + end + if not bb then + logger.warn("[KoInsight] No cover image for:", filepath) + return nil + end + + local dir = ensure_cache_dir() + local tmp = string.format("%s/koinsight_cover_%s.png", dir, tostring(os.time())) + local ok, err = pcall(function() + bb:writePNG(tmp) + end) + bb:free() + + if not ok then + logger.warn("[KoInsight] writePNG failed:", filepath, err) + return nil + end + return tmp +end + +return M diff --git a/plugins/koinsight.koplugin/cover_path_resolver.lua b/plugins/koinsight.koplugin/cover_path_resolver.lua new file mode 100644 index 00000000..7839505e --- /dev/null +++ b/plugins/koinsight.koplugin/cover_path_resolver.lua @@ -0,0 +1,28 @@ +local ReadHistory = require("readhistory") +local logger = require("logger") +local lfs = require("libs/libkoreader-lfs") +local util = require("util") + +local M = {} + +--- Build partial_md5 -> absolute filepath using reading history (existing files only). +function M.buildMd5ToPathMap() + ReadHistory:_read(true) + local map = {} + for _, item in ipairs(ReadHistory.hist) do + local fp = item.file + if fp and lfs.attributes(fp, "mode") == "file" then + local md5 = util.partialMD5(fp) + if md5 then + if map[md5] and map[md5] ~= fp then + logger.dbg("[KoInsight] Duplicate partial md5 in history; keeping first path:", md5) + elseif not map[md5] then + map[md5] = fp + end + end + end + end + return map +end + +return M diff --git a/plugins/koinsight.koplugin/cover_sync.lua b/plugins/koinsight.koplugin/cover_sync.lua new file mode 100644 index 00000000..923bdc84 --- /dev/null +++ b/plugins/koinsight.koplugin/cover_sync.lua @@ -0,0 +1,52 @@ +local logger = require("logger") +local socket = require("socket") +local const = require("./const") +local call_multipart = require("call_multipart") +local cover_extract = require("cover_extract") +local cover_path_resolver = require("cover_path_resolver") + +local M = {} + +--- Upload covers for md5 values the server reported as missing (see POST /api/plugin/import). +function M.syncMissingCovers(server_url, missing_md5_list, quiet) + if not server_url or server_url == "" then + return + end + if type(missing_md5_list) ~= "table" or #missing_md5_list == 0 then + return + end + + local map = cover_path_resolver.buildMd5ToPathMap() + for _, md5 in ipairs(missing_md5_list) do + local path = map[md5] + if not path then + logger.warn( + "[KoInsight] Cover sync skipped (open the book once so it appears in history):", + md5 + ) + else + local png_path = cover_extract.extractCoverToTempPng(path) + if png_path then + local upload_url = server_url .. "/api/plugin/books/" .. md5 .. "/cover" + local ok, err = call_multipart( + upload_url, + { version = const.VERSION }, + png_path, + "file", + "cover.png", + "image/png", + quiet + ) + os.remove(png_path) + if not ok then + logger.warn("[KoInsight] Cover upload failed:", md5, err) + else + logger.info("[KoInsight] Cover uploaded:", md5) + end + end + end + socket.sleep(0.15) + end +end + +return M diff --git a/plugins/koinsight.koplugin/upload.lua b/plugins/koinsight.koplugin/upload.lua index 912bc953..2f0db490 100644 --- a/plugins/koinsight.koplugin/upload.lua +++ b/plugins/koinsight.koplugin/upload.lua @@ -76,6 +76,11 @@ function send_statistics_data(server_url, silent) local ok, response = callApi("POST", url, get_headers(body), body) + if ok and type(response) == "table" and response.missing_cover_md5 then + local cover_sync = require("cover_sync") + cover_sync.syncMissingCovers(server_url, response.missing_cover_md5, silent) + end + if not silent then if ok then render_response_message(response, "Success:", "Data uploaded.") @@ -131,7 +136,11 @@ function send_book_annotations(server_url, book_md5, annotations, total_pages, b } body = JSON.encode(body) - return callApi("POST", url, get_headers(body), body) + local ok, response = callApi("POST", url, get_headers(body), body) + if ok and type(response) == "table" and response.missing_cover_md5 then + require("cover_sync").syncMissingCovers(server_url, response.missing_cover_md5, true) + end + return ok, response end -- Bulk sync all books with annotations